[React] 이미지 다중 업로드 기능 및 미리보기 구현

jellyjw·2024년 9월 13일
1
post-thumbnail
post-custom-banner

글을 쓸 때 이미지도 다중으로 업로드가 가능하고, slack 처럼 내가 업로드 할 이미지가 미리보기로 보여야 했다.

처음 구현해본 기능이라, 어떻게 구현했는지 정리할 겸 기록해보려 한다.

#1. input 의 accept, multiple

<input
  id="file-selector"
  type="file"
  className="hidden"
  accept=".png, .jpg, .jpeg, .gif"
  onChange={handleFileChange}
  multiple
/>

input 태그의 accept 속성을 이용하면, 원하는 파일 형식을 지정해 잘못된 파일 타입을 선택하는 것을 방지할 수 있다. (images/* 와 같은 형태로도 사용 가능)

https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
MDN (input - accept)

그리고 다중 이미지 선택을 허용하려면 multiple 속성만 추가해주면 된다.

#2. input 업로드 버튼 클릭 핸들러 구현

기본 input 태그의 업로드 버튼 UI가 예쁘지 않기 때문에, 아마 대다수가 나처럼 기본 input은 display: none 으로 숨겨놓고 업로드 UI를 따로 만들어서 사용할 것이다.

const clickFileSeletor = () => {
    const fileSelector = document.getElementById('file-selector');
    if (fileSelector) {
      fileSelector.click();
    }
  };

#3. 미리보기 구현

여러개의 파일을 선택하고 업로드할 경우, handleFileChange 함수로 선택한 파일들이 전달될 것이다.
이 때 event.target.files 는 이렇게 객체

// FileList 예시
{
  0: File {name: "image1.jpg", size: 102400, type: "image/jpeg", ...},
  1: File {name: "image2.png", size: 204800, type: "image/png", ...},
  length: 2
}

이 FileList들을 일반 배열로 변환해주고,
uploadImage라는 File[] 타입의 useState 배열 안에 담아준다.


  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = event.target.files;
    if (files) {
      const newFiles = Array.from(files).slice(0, 10); // 최대 10개 파일만 선택
      setUploadImage((prev) => {
        const updatedFiles = prev ? [...prev, ...newFiles] : newFiles;
        return updatedFiles.slice(0, 10);
      });
      ....
      생략
    }

이제, 선택한 파일들의 리스트가 배열에 담겼으니 미리보기 구현을 위해 img 태그 src 속성에 할당할 이미지 경로가 필요한데, 이 때 FileReader 를 사용할 수 있다.

 const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = event.target.files;
    if (files) {
      const newFiles = Array.from(files).slice(0, 10); // 최대 10개 파일만 선택
      setUploadImage((prev) => {
        const updatedFiles = prev ? [...prev, ...newFiles] : newFiles;
        return updatedFiles.slice(0, 10);
      });

      // ** FileReader로 File 읽어 URL 생성
      newFiles.forEach((file) => {
        const reader = new FileReader();
        reader.onloadend = () => {
          const imageUrl = reader.result as string;
          setPreviewUrl((prev) => {
            const updatedUrls = prev ? [...prev, imageUrl] : [imageUrl];
            return updatedUrls.slice(0, 10);
          });
        };
        reader.readAsDataURL(file);
      });
    }
    setImgUploadModal(false);
  };

FileReader는 newFiles (File들이 담긴 배열) 배열을 순회하며 reader.readAsDataUrl(file) 메서드로 File을 읽어 Base64로 인코딩된 데이터 URL을 생성한다.

이 URL을 previewUrl이라는 배열에 담아주고, 섹션에 이미지들을 렌더링해주면 내가 파일을 업로드할때마다 미리보기 이미지들이 생성된다.

https://developer.mozilla.org/en-US/docs/Web/API/FileReader
MDN - FileReader

createObjectURL 대신 FileReader를 사용한 이유

미리보기 구현시 URL.createObjectURL 을 이용하는 방법도 있지만, 이때 생성된 URL이 사용하지 않아도 브라우저 메모리에 계속 남아있어 메모리 누수 문제가 발생할 수 있다고 한다. (URL.revokeObjectURL(objectURL) 을 통해 clear 해줘야 한다고 함)

모든 브라우저에서 지원하는 것을 확인한 뒤 비동기로 동작하는 FileReader를 이용해 주었다.

전체 코드

const [previewUrl, setPreviewUrl] = useState<string[] | null>(null);
const [uploadImage, setUploadImage] = useState<File[] | null>(null);


  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const files = event.target.files;
    if (files) {
      const newFiles = Array.from(files).slice(0, 10); // 최대 10개 파일만 선택
      setUploadImage((prev) => {
        const updatedFiles = prev ? [...prev, ...newFiles] : newFiles;
        return updatedFiles.slice(0, 10);
      });

      newFiles.forEach((file) => {
        const reader = new FileReader();
        reader.onloadend = () => {
          const imageUrl = reader.result as string;
          setPreviewUrl((prev) => {
            const updatedUrls = prev ? [...prev, imageUrl] : [imageUrl];
            return updatedUrls.slice(0, 10);
          });
        };
        reader.readAsDataURL(file);
      });
    }
    setImgUploadModal(false);
  };

// 이미지 렌더링 코드는 많이 생략되었음
{previewUrl?.map((image, idx) => {
 return (
    <img
      src={image}
      alt={`upload-preview-${idx}`}
    />
);
})}
profile
남는건 기록뿐👩🏻‍💻
post-custom-banner

0개의 댓글