React 이미지 업로드 전 미리보기 기능

김래영·2022년 5월 4일
4

Frontend

목록 보기
3/15
post-thumbnail

이미지 업로드 전 미리보기 기능 구현하기

이미지를 미리 보여 주는 기능을 만들려고 생각했을 때 아래 두 가지 방법을 생각했다.

  • 이미지를 서버에 올리고 반환된 이미지 주소로 이미지를 미리 보여주는 방법
  • 이미지를 서버에 올리기 전 이미지를 미리 보여주는 방법

두 번째 이미지를 서버에 올리기 전 이미지를 미리 보여주는 방법을 선택했는데 이유는 사용자가 잘못 이미지를 올렸을 경우 서버에 불필요한 이미지가 저장되는 것을 막고, 사용자가 여러 번 이미지를 교체할 경우 서버에 올리는 시간이 걸리기 때문에 더 나은 사용자 경험을 주기 위해서다.

구현 방법

  • limit을 주었을 때 이미지 업로드 개수를 제한할 수 있는 컴포넌트로 구현하였다.

  • <input />태그로 파일을 불러오기 때문에 <input ref={fileRef}/>태그에 fileRef를 넣고, +를 클릭했을 때(ex: fileRef.current?.click()) onFileChange함수가 실행될 수 있도록 했다.

    •   import React, { useRef, useState } from 'react';
      
        interface Props {
          limit?: number;
        }
      
        const FileInput: React.FC<Props> = ({
          limit,
        }) => {
          const fileRef = useRef<HTMLInputElement>(null);
          /*...*/
          return (
            <div className="flex w-full overflow-auto scrollbar-hide">
              {/*...*/}
              <div>
                <input
                  accept="image/*"
                  ref={fileRef}
                  onChange={onFileChange}
                  type="file"
                  className="hidden"
                />
                <div
                  onClick={() => {
                    fileRef && fileRef.current?.click();
                  }}
                  className="cursor-pointer border-dashed text-[40px] w-[200px] h-[200px] border border-gray-200 mt-5 flex items-center justify-center font-jua text-gray-300"
                >
                  +
                </div>
              </div>
              {/*...*/}
            </div>
          );
        };
      
        export default FileInput;
  • 이미지를 불러올 때 이벤트 함수 onFileChange함수가 작동되면서 이미지 파일을 불러온다.

    • e.target.files 배열에 파일의 정보가 담겨있다.
    •   const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
          const file = e.target.files?.[0];
        };
  • new FileReader()readAsDataURL에 이미지 파일을 넣어준다.

    • FileReader.readAsDataURL

    •   const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
          const file = e.target.files?.[0];
      
            const fileReader = new FileReader();
            fileReader.readAsDataURL(file);
        };
  • new FileReader()onload를 사용해서 파일을 읽는다.

    • e.target.result에 담겨있는 url을 이미지 태그 src에 넣어준다.

    •   const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
          const file = e.target.files?.[0];
      
            const fileReader = new FileReader();
            fileReader.readAsDataURL(file);
            fileReader.onload = (e) => {
              const result = e?.target?.result as string;
              setImgSrcList([...imgSrcList, result]);
            };
        };

전체 구현 소스

  • 아래는 이미지 생성 및 수정을 할 수 있도록 구현한 전체 소스이다.
  • 이미지는 이미지 크기 줄여 업로드하였다.
import React, { useRef, useState } from 'react';
import { useEffect } from 'react';
import { CgClose } from 'react-icons/cg';
import imageCompression from 'browser-image-compression';

interface Props {
  data?: string[];
  limit?: number;
  handleFile: (val: File) => void;
  handleImgSrcList?: (val: string[]) => void;
}

const FileInput: React.FC<Props> = ({
  data,
  limit,
  handleFile,
  handleImgSrcList,
}) => {
  const fileRef = useRef<HTMLInputElement>(null);

  const [imgSrcList, setImgSrcList] = useState<string[]>([]);

  const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];

    const options = {
      maxSizeMB: 2,
      maxWidthOrHeight: 1920,
    };

    if (file) {
      const compressedFile = await imageCompression(file, options);
      const formData = new FormData();
      formData.append('image', compressedFile);
      handleFile(compressedFile);

      const fileReader = new FileReader();
      fileReader.readAsDataURL(compressedFile);
      fileReader.onload = (e) => {
        const result = e?.target?.result as string;
        setImgSrcList([...imgSrcList, result]);
        handleImgSrcList && handleImgSrcList([...imgSrcList, result]);
      };
    }
  };

  const handleRemoveImage = (index: number) => {
    const result = JSON.parse(JSON.stringify(imgSrcList));
    result.splice(index, 1);
    setImgSrcList(result);
    handleImgSrcList && handleImgSrcList(result);
  };

  useEffect(() => {
    if (data?.length) {
      setImgSrcList(data);
      handleImgSrcList && handleImgSrcList(data);
    }
  }, [data?.length]);

  return (
    <div className="flex w-full overflow-auto scrollbar-hide">
      {imgSrcList?.map((el, i) => {
        return (
          <div
            key={i}
            className="relative min-w-[200px] max-w-[200px] min-h-[200px] max-h-[200px] bg-black mt-5 mr-5 flex items-center justify-center"
          >
            <button
              className="absolute top-0 right-0 bg-[#444040b8] text-white w-8 h-8 flex justify-center items-center"
              onClick={() => handleRemoveImage(i)}
            >
              <CgClose />
            </button>
            <img
              alt={'이미지'}
              src={el}
              className="w-full h-full object-contain"
            />
          </div>
        );
      })}
      {limit !== imgSrcList?.length ? (
        <div>
          <input
            accept="image/*"
            ref={fileRef}
            onChange={onFileChange}
            type="file"
            className="hidden"
          />
          <div
            onClick={() => {
              fileRef && fileRef.current?.click();
            }}
            className="cursor-pointer border-dashed text-[40px] w-[200px] h-[200px] border border-gray-200 mt-5 flex items-center justify-center font-jua text-gray-300"
          >
            +
          </div>
        </div>
      ) : null}
    </div>
  );
};

export default FileInput;
profile
개발 노트

0개의 댓글