이미지 파일 여러개 업로드하고 미리보기 렌더링 (React-Typescript)

Devinix·2024년 3월 11일
0
post-thumbnail

useUploadImage 훅

import { ChangeEvent, useRef, useState } from "react";

function useUploadImage() {
  // file 타입 input을 참조하기 위한 ref
  const imageInputRef = useRef<HTMLInputElement | null>(null);
  // 선택된 이미지 파일들을 저장할 상태
  const [imageFiles, setImageFiles] = useState<File[]>([]);
  // 선택된 이미지 파일의 미리보기 URL을 저장할 상태
  const [previewUrls, setPreviewUrls] = useState<string[]>([]);

  // 파일 선택 시의 이벤트 핸들러
  const handleFileChange = (e: ChangeEvent<HTMLInputElement>): void => {
    // 선택된 파일이 있는지 확인
    if (e.target.files) {
      // 선택된 파일들 중에서 최대 10개까지만 처리함. 이미 선택된 파일이 있다면 그 수를 고려해 최대한 받아옴.
      const filesArray = Array.from(e.target.files).slice(
        0,
        10 - imageFiles.length
      );

      // 기존의 이미지 파일들에 새로운 파일들을 추가
      const newImageFiles = [...imageFiles, ...filesArray];
      setImageFiles(newImageFiles);

      // 미리보기 URL을 업데이트하기 위한 임시 배열
      const newPreviewUrls = [...previewUrls];
      
      // 각 파일에 대해 FileReader를 사용하여 미리보기 URL을 생성
      filesArray.forEach((file) => {
        const reader = new FileReader();

        // 파일 읽기가 끝났을 때 실행될 콜백을 정의
        reader.onloadend = () => {
          // 생성된 미리보기 URL을 임시 배열에 추가
          newPreviewUrls.push(reader.result as string);

          // 모든 파일의 미리보기 URL이 준비되었는지 확인하고, 준비되었다면 상태를 업데이트함
          if (newPreviewUrls.length === newImageFiles.length) {
            setPreviewUrls(newPreviewUrls);
          }
        };

        // 파일을 Data URL 형태로 읽음. 이 URL은 미리보기 이미지로 사용됨
        reader.readAsDataURL(file);
      });
    }
  };

  // 이미지를 닫는 함수. 지정된 인덱스의 이미지를 상태에서 제거함
  const handleCloseImage = (index: number): void => {
    // 지정된 인덱스를 제외한 나머지 이미지 파일들을 필터링함
    const newImageFiles = imageFiles.filter(
      (_, fileIndex) => fileIndex !== index
    );
    setImageFiles(newImageFiles);

    // 지정된 인덱스를 제외한 나머지 미리보기 URL들을 필터링함
    const newPreviewUrls = previewUrls.filter(
      (_, urlIndex) => urlIndex !== index
    );
    setPreviewUrls(newPreviewUrls);
  };

  // 이미지 업로더 버튼 클릭 시의 이벤트 핸들러. 실제 파일 입력 요소를 클릭하도록 함 (file 타입의 input은 스타일링이 불가능하기 때문...)
  const handleClickImageUploader = (): void => {
    imageInputRef.current?.click();
  };

  return {
    imageInputRef,
    handleClickImageUploader,
    handleFileChange,
    previewUrls,
    handleCloseImage,
    imageFiles,
  };
}

export default useUploadImage;

컴포넌트 코드

  // 생략...
  return (
    <AddProductDetailSection
      label="이미지 등록"
      limit={`(${previewUrls.length}/10)`}
      type="image"
    >
      <div className={styles.container}>
        <div className={styles.images_container}>
          {previewUrls.length < 10 && (
            <section className={styles.left_section}>
              <div
                onClick={handleClickImageUploader}
                className={styles.add_image_wrapper}
              >
                <img
                  src="/images/Camera.png"
                  alt="Add more"
                  className={styles.camera_icon}
                />
              </div>
              <p className={styles.image_text}>
                {imageFiles.length === 0 ? "썸네일/대표 이미지" : "추가 이미지"}
              </p>
            </section>
          )}

          <section className={styles.right_section}>
            // previewUrls 배열을 기반으로 한 미리보기 렌더링
            {previewUrls.map((url, i) => (
              <div key={i} className={styles.image_wrapper}>
                <div className={styles.image_section}>
                  <img
                    src={url}
                    alt={`Image preview ${i + 1}`}
                    className={styles.image}
                  />
                </div>

                <div
                  // 미리보기 배열 중 선택된 요소 취소
                  onClick={() => handleCloseImage(i)}
                  className={styles.close_icon}
                />
                <p className={styles.image_text}>
                  {i === 0 ? "썸네일/대표 이미지" : "추가 이미지"}
                </p>
              </div>
            ))}
          </section>

          <input
            type="file"
            onChange={handleFileChange}
            ref={imageInputRef}
            multiple
            style={{ display: "none" }}
          />
        </div>
      </div>
    </AddProductDetailSection>
  );
}
profile
프론트엔드 개발

0개의 댓글