[Next.js] 이미지 여러장 업로드하기(feat.미리보기)

해달·2023년 4월 29일
1

리뷰 작성기능과 수정기능을 붙이면서 필요했던 기능 중 하나는
등록 할 이미지, 등록 된 이미지를 유저에게 미리 보여줘야 했다.

수정의 경우에 아래의 조건이 필요했는데
1. 이미 등록된 이미지 제거
2. 새로 등록 될 이미지
수정중에 유저가 취소를 위해 뒤로가기를 했을 때 리뷰의 데이터가 수정이 안되어야 했기 때문에 기존 값에서 새롭게 등록 할 이미지를 우선적으로 보여줘야 했다.

기능을 구현하면서 작성했던 내용을 토대로 정리하려고 한다.


등록

요구사항

  1. 등록 버튼이 눌리기 전까지는 유저가 선택한 이미지만 화면에 렌더링한다
  2. 유저가 선택한 이미지 저장 후 조회 시 보여준다

hook

서버에 data를 body로 FormData 형태의 값을 보내주기로 했다.
headers Content-type의 타입을 multipart/form-data로 전달해주었다.

작성하다보니 궁금해서 찾아보니까 아래와 같은 특징도 있다.

  • fetch 등의 네트워크 메서드가 FormData 객체를 바디로 받는다는 건 FormData의 특징입니다.
headers: {
	'Content-Type': 'multipart/form-data',
};

FormData는 여러개의 메서드를 가지고 있는데 그 중에 append 메서드를 사용해서 name과 value를 가진 폼 필드를 추가 해 줄 수 있다.

나같은 경우에는 전달해줄 data에 image를 forEach 메서드를 사용해 append 해주었는데,
폼은 name이 같은 필드 여러 개를 허용하기 때문에 append 메서드를 여러 번 호출하여 같은 필드를 계속 추가해도 된다.

  uploadImages.imageFiles.forEach((image) => form.append('images', image));

images 미리보기

  1. 업로드 할 이미지의 url과 file 의 값을 가진 객체를 가진 state 하나를 선언
  const [uploadImages, setUploadImages] = useState({
    imageFiles: [],
    imageUrls: [],
  });
  1. 이미지를 업로드
    a. 전달받은 이벤트 객체에 file이 없다면 retrun
    b. 전달받은 데이터를 배열로 만든다.
    c. 새 이미지 배열을 만든다
    - URL.createObjectURL 메서드를 사용하여 blob 값을 저장한다
    d. 업로드 될 이미지들을 state값에 보관한다.
  const handleImageUpload = (e) => {
    if (!e.target.files) return; // a
    const files = e.target.files;
    const fileArray = Array.from(files); //b
   
    //c
    const newImages = Array.from(files, (file) => URL.createObjectURL(file));
    setUploadImages({
      imageFiles: [...uploadImages.imageFiles, ...fileArray],
      imageUrls: [...uploadImages.imageUrls, ...newImages],
    });
  };
  1. 유저가 올린 이미지를 지울 수 있는 delete 함수 만들기
    전달받은 id 값으로 일치하지 않는 값만 남긴다.
  const handleDeleteImage = (id: number) => {
    setUploadImages({
      imageFiles: uploadImages.imageFiles.filter((_, index) => index !== id),
      imageUrls: uploadImages.imageUrls.filter((_, index) => index !== id),
    });
  };
  1. state에서 url 값으로 업로드 될 이미지들을 그려주고 file을 가지고 있는 input을 만들어준다.

file들을 가지고 있을 input은 display:none css 속성을 전달해줘서 보이지 않도록 하고, 유저가 클릭 할 UI는 label로 만들어 유저에게 보여준다.

  {uploadImages.imageUrls.map((url, idx) => <UploadedImage/> )}
                              
	//보이지 않는 파일 업로드 인풋
	<Input
        id='images'
        type='file'
        multiple 
        accept='image/*' 
      />
   //input Id와 htmlFor 연결 
   <ImageUploadUI id='images' htmlFor='images'/>  
  • type=file
    - 파일을 하나 혹은 여러개 선택할 수 있다
  • mulitple
    - 둘 이상의 값을 입력한다고 명시해주기
  • accept=image/*
    - type의 속성값이 file인 경우만 사용 가능
    • 서버에 업로드 할 수 있는 파일의 타입 명시

blob이란 ?

  • 파일류의 불변하는 미가공 데이터 객체
  • 텍스트와 이진 데이터 형태로 읽을 수 있다
  • URL.createObjectURL() 메서드를 사용해서 blob을 url로 변환할 수 있다.
    - url은 생성 된 window의 documnet에서만 유효하다(창을끄면 확인할 수 없다)

수정

저장하기 이전에 유저에게 보여줄 url을 가지고 있을 State 생성
초기값은 (서버에)이미 등록되어 있는 UrlList

  const [images, setImages] = useState<{ file: File[]; blob: string[] }>({
    file: [],
    blob: imageUrlList,
  });

새롭게 이미지를 전달 받은 경우에는
1. 위에 선언해놓은 기존 urlList의 blob값을 저장하고
2. 새 데이터의 files 가공해 배열로 만들어 준 뒤 url 생성 , 파일데이터
이미지는 배열의 맨 앞에 추가해주고
새롭게 업로드 할 file은 배열로 만들어준 뒤 state에 저장해 놓은 뒤
업로드 진행할 때 form.append 값으로 image 데이터를 추가해준다

  1. images state로 서버에 이미지 업로드 요청
    • 새롭게 등록할 이미지
  2. 이미지 업로드 요청 성공 후 전달받은 url data로
    현재 유저에게 보여지고 있는 images state값을 변경시켜준다
   setUpdateReviewState((prev) => {
        return { ...prev, imageUrlList: [...prev.imageUrlList, ...data] };
      });

  const handleImageUpload = (e) => { 
    const imageBlobs = [...images.blob];
    
    const imageFile = Array.from(files).map((file) => {
      const imageUrl = window.URL.createObjectURL(file); // blob:http://localhost:3000/iamge 임시주소
      imageBlobs.unshift(imageUrl);
      return file; //file 데이터
    });

    setImages({ file: imageFile, blob: imageBlobs });
  };

처음 생성때에도 여러개의 이미지를 어떻게 보여주고 값을 어떻게 저장해야 할 지에 대해서 많이 헤맸었다.
초기에는 file과 image를 가진 값을 각각의 state로 구현하였다가,
결국 두 값다 하나의 files라는 값에서 얻어와지는 값이기 때문에 분리하는 것보다는
결합성을 위해 다시 하나의 state로 합치게 되었다.


reference

0개의 댓글