이미지 최적화(미리보기)

박경찬·2022년 9월 1일
0

이미지를 어떻게 하면 가장 효율적으로 불러올까?
아마 나와 같은 초보 개발자들은 한번쯤은 고민해 봤을거라고 생각한다. 분명 이미지를 업로드 하고 불러오는거 까지는 문제가 없는데 성능문제로 시간이 오래 걸리는 현상을 분명 경험 했을?! 거라고 생각한다.
물론 난 이미지 최적화 시키기 위해 많은 고생을 했다..
하지만 까먹었ㄷ ㅑ.. 그래서 다시 리마인드 겸 까먹지 않기 위해 남긴다..

자 우선 이론부터 살펴보자.

Storage

Storage는 쉽게 말해 컴퓨터다.
컴퓨터가 여러개의 데이터를 입력할수 있게끔 여러대의 컴퓨터를 연결시켜놨다 즉 파일을 무제한으로 넣을수 잇게끔 준비된 컴퓨터를 Storage 라고 한다. storage 설명은 여기까지만!☝🏼

B = API
Br = 브라우저
DB = 데이터베이스

B에 보면 uploadFile , createBoard 2개의 API가 있다.

Br 에서 특정 이미지파일을 선택해서 업로드 버튼을 클릭하게되면uploadFile API가 호출된다. 이때 백엔드 에서는 파일을 받아서 storage 에 올리고 해당하는 URL를 받아온다. 받아온 URL를 다시 프론트엔드 쪽으로 보내준다.

프론트 엔드에서는 받은 URLimg 태그를 사용해 이미지 미리보기를 제공한다.

이후 게시글을 등록하게되면 createBoard API가 호출된다.
이때 이미지URL,게시글 제목, 내용들이 함께DB 저장된다.

이미지를 불러올때는 저장되어 있는 URL를 fetch 하여 보여준다.
여기까지가 이제 이미지를 업로드 하고 저장후 보여주기 까지의 프로세스이다.

2가지의 문제점😟

첫번째 ☝🏼 문제점은 onChange 함수를 사용해서 파일을 stroage 로 업로드 하고 백엔드에서 받은 URL를 통해 미리보기를 하는데 생각보다 바로 나오지 않고 시간이 소모된다.


두번째 ✌🏻이미지를 1개이상 업로드 하고 미리보기 까지 완료된 상태에서 게시글을 등록하지 않고 다른페이지로 이동하면 이미 uploadFile API 가 호출되어 storage에 파일들이 쌓여 있다.
이렇게 되면 필요없는 데이터들이 storage에 쌓이게 되고 비용까지 부담 해야하는 문제점이 있다.

우리가 해결할수 있다고 하면 어떤 방법을 해야할까? 위 내용을 보면 게시글을 등록하기전 API가 호출된다! 바로 uploadFile 이다.
그렇다면? API를 createBoard 와 동시에 호출할수 있게 된다면 어떨까?
불필요한 데이터들이 쌓이지 않고 비용측면에서도 절감할수 있다!🙌

첫번째☝🏻

문제 부터 생각해보면 이미지를 선택하고 업로드 할때 API호출이 아니라 자바스크립트에서 임시 URL를 만들어 보여주게 하면 미리보기가 빨라 지지 않을까? 여기서 의문점이 한가지 있다.저장할때도 임시 URL를 저장하나? 임시 URL를 DB에 저장할수는 없다 이유는? 임시URL이기 때문이다. 자세한 이유는 밑에서 설명하겠다!

두번째✌🏻

게시글 등록할때 uploadFile => createBoard 순서로 묶어서 호출하면 된다! 등록버튼을 클릭했을때 호출되는 함수를 사용해 이미지 업로드를 하고 받은 URL로 게시글도 등록하면 되지 않을까? 2개의 함수를 묶어버리면 되지 않을까? 그렇게 된다면 storage에 불필요한 데이터가 쌓이지 않을거고 비용도 줄일수 있다!

기존 코드의 함수 내용이다

const onChangeFile = async (event: ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    console.log(file);

    try {
      const result = await uploadFile({ variables: { file } });
      setImageUrl(result.data?.uploadFile.url);
    } catch (error) {
      if (error instanceof Error) Modal.error({ content: error.message });
    }
  };

위내용을 보면 타깃에 파일을 file변수에 담아서 바로 uploadFile API를 호출한다. URL를 받아와서 setUrlImage를 해주게된다. 이렇게 되면 성능이 저하되고 storage에 찌거기를 남기게된다.

필자는 new fileReader()를 사용해 URL를 만들어 브라우저에 저장하는 방법을 선택했다.

코드 작성을 하다가 set 하는 과정에서 빨간줄..
이유는 state에서 문자열로 정해주는 바람에 이미 스트링으로 타입이 추론되어서 그렇다.
데이터가 존재할수도 있고 없을수도 있기 때문에 제어문을 작성해서 오류를 해결하면된다. 간단하다.

 if (typeof data.target?.result === "string") {
       
        setImageUrl(data.target?.result);
      }

import { ChangeEvent, useState } from "react";

export default function ImageUploadPreviewPage() {
  const [imageUrl, setImageUrl] = useState("");

  const onChangeFile = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]; //파일이 있을수도 없을수도 있을경우 옵셔널 체이닝을 사용한다.
    if (!file) {
      alert("파일이 없습니다");
      return;
    }
    const fileReader = new FileReader();
    fileReader.readAsDataURL(file);
    fileReader.onload = (data) => {
      if (typeof data.target?.result === "string") {
        console.log(data.target?.result);
        setImageUrl(data.target?.result);
      }
    };
  };

  return (
    <div>
      <input type="file" onChange={onChangeFile} />
      <img src={imageUrl} />
    </div>
  );
}

미리보기 코드는 작성했다. 테스트 해보자 테스트 해서 중요하게 볼내용은 console에 이미가 어떤 형태로 보이는지 확인해야한다. 임시 url을 저장하면 안되는 이유는 그다음 설명하겠다.

임시 url들이 보이는가?..🫢

이대로 저장하면 안되나?? 답은 저장해도 상관은 없다! 다만 하지 않는 이유는 정형화 되어 있지 않는 문자와 숫자가 이미지 크기 만큼 데이터로 저장된다는거다 이렇게 되면 DB에도 불필요한 데이터가 들어가게 되고 쓸데없이 데이터 자리만 차지한다. DB에도 데이터 타입별로 저장할수 있는 크기가 제한적이기 때문이다. 프론트에서도 깊이는 아니지만 왜이렇게 저장하면 안되는 이유는 알고 넘어가는게 좋을거 같아 남긴다.


file변수를 인자값으로 넣게 되면 파일을 읽어서 임시 url를 만들어준다.

인자값으로 Blob를 볼수 있는데 엔티티로서 저장되는 이진 데이터의 모임이다. DB관리를 해본 사람이라면 blob을 한번쯤은 봤을거다. 비 전통적인 데이터 보관용이라고도 한다. 보통 오디오나, 이미지 , 덩어리가 큰 데이터들을 저장할때 볼수 있다.

의미는 크게 신경쓰지말자! 간단하게라도 알고 넘어가면 좋을거 같아서 남긴다!

자 미리보기를 최적화 시켜봤다 그럼 이제 실제로 DB에 저장 할수 있도록 코드를 작성해보자!

export default function ImageUploadPreviewPage() {
  const [imageUrl, setImageUrl] = useState("");
  const [fileupload, setFileUpload] = useState<File>();
  const [uploadFile] = useMutation<
    Pick<IMutation, "uploadFile">,
    IMutationUploadFileArgs
  >(UPLOAD_FILE);

  const [createBoard] = useMutation(CREATE_BOARD);

  const onChangeFile = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]; // 파일이 있을수도 없을수도 있을경우 옵셔널 체이닝을 사용한다.
    if (!file) {
      alert("파일이 없습니다");
      return;
    }
    const fileReader = new FileReader();
    fileReader.readAsDataURL(file); // 파일을 읽어서 임시 url로 만들어준다.
    fileReader.onload = (data) => {
      // fileReader.readAsDataURL(file) 만들어진 url를 읽는다
      if (typeof data.target?.result === "string") {
        // state에서 스트링으로 추론을 하기 때문에 제어문으로 스트링일때만 URL이 저장될수 있도록 한다.
        setImageUrl(data.target?.result);
        setFileUpload(file);
      }
    };
  };

  const onClickSubmit = async () => {
    try {
      const upload = await uploadFile({ variables: { file: fileupload } });
      const url = upload.data?.uploadFile.url;
      const result = await createBoard({
        variables: {
          CreateBoardInput: {
            writer: "Chan",
            title: "image",
            content: "이미지 업로드",
            images: [url],
          },
        },
      });
      console.log(result.data?.createBoard._id);
    } catch (e: any) {
      Modal.error({ content: e.message });
    }
  };

  return (
    <div>
      <input type="file" onChange={onChangeFile} />
      <img src={imageUrl} />
      <button onClick={onClickSubmit}> 게시글 등록하기</button>
    </div>
  );
}

첫번째 미리보기 해주는 코드에서 달라진점은 state가 추가되었다.

const [fileupload, setFileUpload] = useState<File>();

실제로 DB에 저장되어질 데이터를 담을 state를 만들어줘야한다.

만들어준 이유는 임시URL은 앞에서 저장하지 않는다. 그러므로 앞에서 해준 setImageUrl은 미리보기 용이다 절대 DB로 저장되어 지는 state는 아니다!

onClickSubmit 함수 내용을 자세히 보면 uploadFile , createBoard가 순서대로 수행되도록 작성했다. 게시글 등록하기 버튼을 누르지 않는 이상 뒤로가기 해도 stroage에는 찌꺼기가 쌓이지 않는다!

const upload = await uploadFile({ variables: { file: fileupload } });

실제로 위 코드가 이제 storage로 쌓이게 해주는 코드이다.
여기서 백엔드로 요청하고 백엔드에서는 storage로 데이터를 옮겨준다.

const url = upload.data?.uploadFile.url;

새로 생성된 URL를 변수에 담아 게시글 생성하는 API를 호출해주면 끝이다!

자 다음시간엔 이미지를 여러개 올릴때 어떤 최적화 방법이 있는지 공부해보쟈~~

0개의 댓글