Space
article thumbnail
반응형

Before you learn

🔗 File

🔗 blob

 

🔗 FormData

🔗 formData.append()

 

🔗 FileReader

🔗 FileReader.readAsDataURL()


영상

 

반응형

[React & Next.js] 프로필 이미지 (업로드, 미리 보기, 삭제)

파일 구조

signup.jsx > profileImage.jsx

 

 

미리 보기

이미지 미리 보기 기능을 구현한다면

이미지 File 자체를 읽어서 화면에 보여줄 수 없기에

이미지 파일을 Base64 Data URL로 변환하여 화면에 보여줘야 한다.

(File -> Base64 Data URL)

 

삭제

유저가 선택한 이미지를 삭제한 경우,

기본 이미지로 변경한다. (기본 이미지 위치 : /public/~~~.png )

 

// ProfileImage.jsx

import DeleteBtn from "./Buttons/DeleteBtn";

export default function ProfileImage({ profileImg, setProfileImg }) {
// const [profileImg, setProfileImg] = useState("/profile_basic_img.png"); <--  부모 컨포넌트에 있음

// 프로필 이미지 핸들러
  const handleProfileImage = async (event) => {
    // 하나의 이미지만 선택
    const file = event.target.files[0];

    // File을 선택하지 않은 경우
    if (!file) {
      return;
    } else {
      // File을 선택했다면 기존 값 초기화 시키기
      setProfileImg("");

      const reader = new FileReader();

      // file을 Base64 Data URL로 변환
      // 아래 작업은 console.log를 했을 때 비동기 작업이 완료되지 않아 undefined로 나타난다.
      reader.readAsDataURL(file);

      // 이미지 화면에 보여주기
      // 변환된 Base64 Data URL의 onload가 완료된 후 실행
      reader.onload = (event) => {
        // 변환된 Base64 Data URL의 onload가 완료 2, 진행 중 1, 실패 0 을 반환한다.
        if (reader.readyState === 2) {
          const imgUrl = event.target.result;
          // 상태 업데이트
          setProfileImg(imgUrl);
        }
      };
    }
  };
  
  // 프로필 이미지 삭제 (= 기본 이미지로 변경)
  const deleteImage = () => {
    setProfileImg("/profile_basic_img.png");
  };

  return (
    <div className="w-full flex justify-center">
      <div className="relative">
        <img
          className="relative w-[200px] aspect-square rounded-full border-[1px] cursor-pointer"
          src={profileImg}
          alt="프로필 이미지"
          onClick={() => {
            document.querySelector("#img").click();
          }}
        />
        <div className="absolute top-0 right-0">
          <DeleteBtn onClick={deleteImage} />
        </div>
      </div>

      <input
        id="img"
        className="hidden"
        type="file"
        accept="image/*"
        onChange={handleProfileImage}
      />
    </div>
  );
}

업로드

"Content-Type": "multipart/form-data" 형식으로 서버에 전송해야한다.

 

업로드를 하려면

미리 보기에서 했던 것을 반대로 하면 된다.

미리 보기는 Base64 Data URL로 사진을 보여주기 때문에,

업로드하려면 다시 File 객체로 변환시켜주어야 한다.

(Base64 Data URL -> File)

 

만약 미리 보기 기능을 구현하지 않고, 업로드 기능을 구현한다면, 

유저가 선택한 파일 그 자체를 바로 윗 형식에 맞춰서 보내면 되기에

Base64 Data URL -> File로 변환할 필요가 없다.

 

Base64 data URL

 

File

 

// signup.jsx

import axios from "axios";
import { useState } from "react";
import ProfileImage from "@/components/ProfileImage";

export default function SignUp() {
  // 프로필 사진 (기본 이미지 : "/profile_basic_img.png")
  const [profileImg, setProfileImg] = useState("/profile_basic_img.png");
  
  // Base64 데이터 URL을 Blob으로 변환
  const convertDataURLToFile = async (dataURL, fileName) => {
    const response = await axios.get(dataURL, {
      responseType: "blob",
    });
    const blob = response.data;

    // Blob을 File 객체로 변환
    const profileImgFile = new File([blob], fileName, { type: blob.type });

    return profileImgFile;
  };

  const onClickSubmitBtn = async () => {
    // API 요청을 보내기 위한 FormData 객체 생성
    const formData = new FormData();
    // stay 데이터(이미지를 제외한 나머지 데이터)를 JSON 형식으로 추가
    formData.append(
      "stay",
      new Blob(
        [
          JSON.stringify({
            // 이미지를 제외한 다른 데이터(ex: userName)를 여기에 추가합니다.
          }),
        ],
        { type: "application/json" }
      )
    );

    if (profileImg) {
      console.log("파일 객체로 변환 전 이미지", profileImg);
      
      // Base64 데이터 URL을 Blob으로 변환
      const profileImgFile = await convertDataURLToFile(
        profileImg,
        `profileImg`
      );
      
      formData.append("image", profileImgFile);
      console.log("파일 객체로 변환 후 이미지", profileImgFile);
    }

    try {
      // 서버 API 호출
      const response = await axios.post(
        `${process.env.NEXT_PUBLIC_SERVER_URL}/user/signup`,
        formData,
        {
          headers: {
            // File 데이터를 보낼 경우, "multipart/form-data"로 보내야한다. 
            "Content-Type": "multipart/form-data",
            "ngrok-skip-browser-warning": "69420",
          },
        }
      );
      console.log(response);

    } catch (error) {
      console.log(error);
    }
  };

  return (
    <>
      <main>
        <form onSubmit={(event) => event.preventDefault()}>
          ...
            <section>
              ...
              <div>
                <ProfileImage
                  profileImg={profileImg}
                  setProfileImg={setProfileImg}
                />
              </div>
            </section>
          ...
          <button 
            type='submit'
            onClick={onClickSubmitBtn}
          >
            회원가입
          </button>
        </form>
      </main>
    </>
  );
}
//

 


Add

만약 회원가입 후 개인정보 수정을 통해 프로필 사진을 변경한다면,

서버로부터 가져오는 데이터를 통해

 

지금까지 했던 것과 마찬가지로

미리 보기는 URL로 보여주고, 다시 저장할 프로필 사진은 File 객체로 변환하여

"multipart/form-data" 형식으로 서버에 요청 보내면 된다.

 

반응형
profile

Space

@Space_zero

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!