반응형
Before you learn
영상
반응형
[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로 변환할 필요가 없다.
// 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" 형식으로 서버에 요청 보내면 된다.
반응형