이미지 다중 업로드 및 미리보기

성석민·2022년 1월 14일
25

프론트엔드

목록 보기
3/7
post-thumbnail

리액트에서 어떻게 이미지를 여러장 업로드 시키고 미리보기를 어떻게 구현하는지 설명하려고 한다.
우선 input[type="file" multiple]을 통해 이미지를 업로드하면 다음과 같은 결과를 얻을 수 있다.

단일 이미지

다중 이미지

다중 업로드는 multiple 속성을 입력해주기만 하면 그리 어려운 문제는 아니다.
하지만 미리보기 기능은 위의 결과값에서 없는 상대경로를 알아내야하기 때문에 조금 까다롭다.

그렇다면 상대경로는 어떻게 알아낼 수 있을까 ?

정답은 바로 URL.createObjectUrl() 메소드를 사용하는 것이다.
이미지를 선택 후 받아온 file객체를 URL.createObjectUrl() 메소드의 매개변수로 넣어주게 되면 아래와 같은 상대경로를 반환 받을 수 있다.

✅ 이제 다중 업로드와 미리보기 기능을 구현하기 위해 알아야할 이론은 다 배웠다. ✅

이제 코드로 구현해보자.
↓로직은 아래와 같다.↓

⌲ input[type="file"]의 디자인을 바꾸기 위해서 label 태그를 사용한다.
⌲ label 태그에 onChange 이벤트를 등록한다.
⌲ onChange의 event객체를 이용해서 이미지 데이터들을 받아온다.
⌲ 받아온 이미지들을 순회하면서 URL.createObjectUrl()의 매개변수로 이미지를 넣고 상대경로를 반환 받는다.
⌲ 반환 받은 상대경로를 배열로 관리하고 useState를 통해 데이터를 관리한다.
⌲ 상대경로를 가지고 있는 배열을 순회하면서 img태그의 src 속성에 값을 할당하고 렌더링 시킨다.

🔐 최대 업로드 할 수 있는 이미지를 10개로 제한한다.

 const handleAddImages = (event) => {
    const imageLists = event.target.files;
    let imageUrlLists = [...showImages];

    for (let i = 0; i < imageLists.length; i++) {
      const currentImageUrl = URL.createObjectURL(imageLists[i]);
      imageUrlLists.push(currentImageUrl);
    }

    if (imageUrlLists.length > 10) {
      imageUrlLists = imageUrlLists.slice(0, 10);
    }

    setShowImages(imageUrlLists);
  };

❗️ FileList는 배열이 아닌 객체형태로 반환된다. 그렇다면 length 속성을 이용해서 순회할 수 있을까 ?

FileList는 기본적으로 length라는 속성을 갖는다. length 속성을 이용해서 순회를 했다고 배열이라고 착각하면 안된다.

MDN-FileList


전체 소스코드

const Images = () => {
  const [showImages, setShowImages] = useState([]);

  // 이미지 상대경로 저장
  const handleAddImages = (event) => {
    const imageLists = event.target.files;
    let imageUrlLists = [...showImages];

    for (let i = 0; i < imageLists.length; i++) {
      const currentImageUrl = URL.createObjectURL(imageLists[i]);
      imageUrlLists.push(currentImageUrl);
    }

    if (imageUrlLists.length > 10) {
      imageUrlLists = imageUrlLists.slice(0, 10);
    }

    setShowImages(imageUrlLists);
  };

  // X버튼 클릭 시 이미지 삭제
  const handleDeleteImage = (id) => {
    setShowImages(showImages.filter((_, index) => index !== id));
  };

  return (
    <div className={classes.addPicture}>
      <label htmlFor="input-file" className={classes.addButton} onChange={handleAddImages}>
        <input type="file" id="input-file" multiple className={classes.addButton} />
        <Plus fill="#646F7C" />
        <span>사진추가</span>
      </label>

      // 저장해둔 이미지들을 순회하면서 화면에 이미지 출력
      {showImages.map((image, id) => (
        <div className={classes.imageContainer} key={id}>
          <img src={image} alt={`${image}-${id}`} />
          <Delete onClick={() => handleDeleteImage(id)} />
        </div>
      ))}
    </div>
  );
};

틀린 부분이 있거나 보충해야 할 내용이 있다면 댓글이나 DM(sungstonemin)으로 알려주시면 감사하겠습니다😄

profile
기록하는 개발자

4개의 댓글

comment-user-thumbnail
2022년 1월 26일

createObjectURL 꿀 정보네요 👍

답글 달기
comment-user-thumbnail
2022년 4월 27일

좋은 정보 공유주셔서 감사합니다. createObjectURL을 통해 만들어진 url은 해당 브라우저가 존재한 상태에서 무효화시키지 않으면 메모리 누수가 일어난다고 하네요! 보시는 분들 하단 링크 참고하셔도 좋을 것 같아요 :)
https://kyounghwan01.github.io/blog/JS/JSbasic/Blob-url/#createobjecturl

답글 달기
comment-user-thumbnail
2022년 7월 21일

밑에 Plus나 Delete는 어디서 import하셨나요?

1개의 답글