언어/Next.js

[Next 고캠핑] FileReader 이용해 이미지 보여주기

홍시_코딩기록 2024. 9. 5. 22:51

 

 

 

캠핑장 선택은 진행 전

 

캠핑장 후기를 작성하고 이미지가 추가할 경우 FileReader로 이미지가 미리 보여지며

등록하기를 누르면 파이어베이스 storage에 저장이 된다.

이미지는 4개로 제한해놨기때문에 4개가 초과되면 alert창이 뜬다.

 

 

firebase database/ storage

 

 

firebase 들어가면 확인할 수 있다. 내가 작성한 필드와 스토리지에 저장된 이미지들.

 

 

 

FileReader

정의 :

  • FileReader 객체는 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여 읽을 파일을 가리키는File 혹은 Blob 객체를 이용해 파일의 내용을(혹은 raw data버퍼로) 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 함.
  • 비동기적으로 동작함.

 

Base64

  • 8비트 2진 데이터를 (플랫폼의) 문자 코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 가리키는 개념
  • Base64는 데이터 크기를 약 33% 정도 증가시키지만, 텍스트로 변환되기 때문에 네트워크 통신, 이메일 전송 등에서 바이너리 데이터를 안전하게 사용할 수 있음.
  • 이미지와 같은 파일은 바이너리 데이터로 이루어져 있는데, 이 바이너리 데이터를 HTML이나 JSON 등의 텍스트 기반 문서에서 바로 사용할 수 없다. 이걸 해결하기 위해 이미지를 Base64로 인코딩하여 HTML의 <img> 태그 등에서 바로 사용할 수 있게 변환

 

파일들을 Base64로 변환하는 함수

  const [postImg, setPostImg] = useState<string[]>([]); // 이미지 미리보기 상태
  
  const encodeFileToBase = async (fileList: File[]) => { 
    const encodingFiles = fileList.map(async (file) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      return await new Promise((resolve) => {
        reader.onload = () => {
          resolve(reader.result);
        };
      });
    });

    const convertFiles = await Promise.all(encodingFiles);
    setPostImg([...postImg, ...convertFiles] as string[]);
  };

 

- 유저가 선택한 이미지 파일들을 Base64 형식으로 변환하고 미리보기 이미지를 데이터를 생성한다.

  • fileList: 유저가 업로드한 파일들의 배열
    이 배열에 있는 파일들을 하나씩 Base64로 변환하기 위해 map을 사용.
  • encodingFiles: 각 파일들을 비동기로 처리하기 위한 배열.
  • FileReader: 파일을 읽을 수 있게 해주는 객체로 파일을 읽어서 Base64 으로 변환. reader로 선언
  • readAsDataURL(file): 파일을 Base64형식의 Data URL로 변환하는 메서드(파일을 텍스트 형식으로 변환)
  • onload: 파일 읽기가 완료되면 Base64로 변환된 이미지 데이터 reader.result를 resolve로 반환.
  • convertFiles: Promise.all() 모든 파일이 Base64로 변환 될 때까지 기다렸다가 그 결과를 convertFiles에 저장.
  • setPostImg: 변환된 파일들을 postImg상태에 추가해서 이미지 미리보기에 사용.

 

input 파일 onChange 이벤트

const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      const selectedFiles = Array.from(e.target.files); // 선택 파일들을 배열로 변환한다.

      if (postImg.length + selectedFiles.length > 4) {
        // 이미지가 4개 초과이면 경고창
        openModal("registerImage");
        return;
      }

      const formData = new FormData();
      selectedFiles.forEach((file) => {
        formData.append("file", file);
      });

      void encodeFileToBase(selectedFiles); // 선택된 파일들을 Base64로 변환하여 미리보기에 저장
      onImageSelected(selectedFiles); // 부모 컴포넌트로 전달
    }
  };
  • e.target.files: 유저가 선택한 파일들의 리스트 selectedFiles에 배열로 저장됨.
  • Array.from(e.target.files): encodeFileToBase의 fileList를 콘솔로 찍어보면 배열처럼 보이지만 배열이 아니라 유사배열 객체라고 한다.(Array.isArray(e.target.files)로 찍어보면 false가 나옴)
    그래서 fileList를 일반 배열로 변환.
  • new FormData(): FormData는 파일 데이터를 전송할 때 사용된다. 
  • encodeFileToBase(selectedFiles): 배열로 저장한 selectedFiles를 encodeFileToBase 함수로 호출하여 Base64 형식으로 변환. 변환이 모두 완료되면 setPostImg를 통해 기존 상태에 변환 된 이미지가 추가. 
  • onImageSelected: 부모 컴포넌트로 전달.

 

 

처음엔 파이어베이스 스토리지를 사용해서 해야하나 했으나 불필요한 데이터 사용이라는 생각이 들어서

다른 방법을 찾아봤다. 역시나 똑똑한 사람들은 많다 ㅎㅎ 블로그와 지피티..를 참고하여 완료해봤다

파이어베이스 스토리지 올리기까지 쓰려 했으나,

캠핑장 선택이 아직 진행 안된 관계로 모두 완료한 다음 파이어스토어 등록 게시글을 쓰려한다.

 

 

 

UploadImage 전체 코드

더보기
interface IPropsImageUpload {
  onImageSelected: (files: File[]) => void; // 부모 컴포넌트에 넘겨주는 이미지 파일 함수
}
export default function UploadImage({ onImageSelected }: IPropsImageUpload) {
  const [postImg, setPostImg] = useState<string[]>([]); // 이미지 미리보기 상태
  const fileEl = useRef<HTMLInputElement>(null); // 파일 input에 접근하는 useRef
  const { currentModal, openModal, closeModal } = useModal();

  // 파일들을 Base64로 변환하는 함수
  const encodeFileToBase = async (fileList: File[]) => {
    const encodingFiles = fileList.map(async (file) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      return await new Promise((resolve) => {
        reader.onload = () => {
          resolve(reader.result);
        };
      });
    });
    console.log(fileList);

    const convertFiles = await Promise.all(encodingFiles);
    setPostImg([...postImg, ...convertFiles] as string[]);
  };

  // input 파일 onChange 이벤트
  const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files) {
      const selectedFiles = Array.from(e.target.files); // 선택 파일들을 배열로 변환

      if (postImg.length + selectedFiles.length > 4) {
        // 이미지가 4개 초과이면 경고창
        openModal("registerImage");
        return;
      }

      const formData = new FormData();
      selectedFiles.forEach((file) => {
        formData.append("file", file);
      });

      void encodeFileToBase(selectedFiles); // 선택된 파일들을 Base64로 변환하여 미리보기에 저장
      onImageSelected(selectedFiles); // 부모 컴포넌트로 전달
    }
  };

  // 이미지 삭제 이벤트
  const onClickDeleteImage = (index: number) => {
    const deleteImage = [...postImg];
    deleteImage.splice(index, 1);
    setPostImg(deleteImage);
  };
  return (
    <Wrap>
      <input
        type="file"
        accept="image/*"
        ref={fileEl}
        name="reviewImage"
        id="reviewImage"
        multiple
        onChange={onChangeImage}
      />
      <ImageWrap>
        <button
          className="upload_btn"
          type="button"
          onClick={() => fileEl.current?.click()} // input file 참조
        >
          <span className="sr_only">파일 업로드</span>
          <LuPlus />
        </button>
        <ul className="image_list">
          {postImg.map((imgSrc, i) => (
            <li key={i}>
              <button
                type="button"
                className="delete_image"
                onClick={() => {
                  onClickDeleteImage(i);
                }}
              >
                <IoIosClose />
                <span className="sr_only">이미지 제거</span>
              </button>
              <img src={imgSrc} alt={`imagePreview${i}`} />
            </li>
          ))}
        </ul>
      </ImageWrap>
      <p className="guide">최대 4개의 이미지만 선택 가능합니다.</p>

      {/* 내캠핑장 로그인 alert */}
      {currentModal === "registerImage" && (
        <Modal
          currentModal={currentModal}
          hide={closeModal}
          message="최대 4개의 이미지만 선택 가능합니다."
        />
      )}
    </Wrap>
  );
}

 

https://dev-102.tistory.com/entry/FileReader-multiple-files-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

https://ramincoding.tistory.com/entry/ReactProject-input-%EC%9C%BC%EB%A1%9C-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%9C-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%95%98%EB%82%98%EC%94%A9-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0


https://velog.io/@acwell94/firebase%EC%97%90%EC%84%9C-storage%EC%97%90-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95