트러블슈팅인가..? 그건 아니지만 느낀점이 많았던 작업이라 TIL로 남겨보기로 했다.
마이페이지에 사진이나 개인정보를 수정하는 기능을 구현해야 했는데 간단한 부분이지만 참 고생길을 걸었다...🫠
사진 수정 업로드 구현하기
본격적으로 팀프로젝트 기능구현하기 전에 React-hook-form
이랑 Zod
를 써보기로 했었다. supabase
도 어려운데 강의로만 공부하던 걸 적용하기 어려울 것 같아서 미리 공부도 간단하게 했었다.
이걸 꼭 써보고 싶어서 인증/인가 부분을 하고 싶었는데 아쉽게도 그 부분은 다른 팀원이 맡게 되었다.
그러다가 이미지 수정 쪽에 자기소개랑 닉네임 수정 폼이 있어서 여기에 써볼 수 있지 않을까? 해서 좀 신이 났었다!! 공부한 걸 써볼 수 있겠구나!!
아무튼 그렇게 작성한 코드는 이랬다.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { supabase } from "@/supabase/supabase";
import { CircleCheckBig } from "lucide-react";
const profileSchema = z.object({
profileImage: z
.any()
.refine((file) => file instanceof File && file.size <= 5 * 1024 * 1024, {
message: "5MB 이하의 파일만 가능합니다."
})
.refine((file) => ["image/jpeg", "image/png", "image/gif"].includes(file.type), {
message: "JPEG, PNG, GIF 이미지 파일만 가능합니다."
})
});
type ProfileFormData = z.infer<typeof profileSchema>;
interface ProfileImageUploadProps {
userId: string;
onImageUpload: (url: string) => void;
}
const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({ userId, onImageUpload }) => {
const {
register,
handleSubmit,
setValue,
formState: { errors }
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema)
});
// 이미지 업로드 함수
const onSubmit = async (data: ProfileFormData) => {
console.log("onSubmit 할 때 data:", data);
console.log("userId 확인", userId);
const file = data.profileImage;
if (!file) {
console.log("파일이 없습니다.");
return;
}
const { data: uploadData, error: uploadError } = await supabase.storage
.from("zipbob_storage")
.upload(`userProfileFolder/${userId}`, file, { upsert: true });
if (uploadError) {
console.error("사진 업로드 오류", uploadError.message);
return;
}
const profileImageUrl = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/zipbob_storage/userProfileFolder/${userId}`;
console.log("이미지 업로드 성공:", profileImageUrl);
const { error: updateError } = await supabase
.from("USER_TABLE")
.update({ user_img: profileImageUrl })
.eq("user_id", userId);
if (updateError) {
console.log("USER_TABLE 업데이트 오류 발생", updateError.message);
return;
}
onImageUpload(profileImageUrl);
console.log("USER_TABLE 업데이트 성공!", profileImageUrl);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col items-center">
<input
type="file"
accept="image/*"
{...(register("profileImage"), { required: true })}
onChange={(e) => {
const file = e.target.files?.[0];
console.log("File selected:", file);
if (file) {
setValue("profileImage", file, { shouldValidate: true });
}
}}
/>
{errors.profileImage && <p className="text-red-500 text-sm mt-1">{String(errors.profileImage.message)}</p>}
<button type="submit" className="flex gap-2 align-middle">
이미지 수정
<CircleCheckBig size={16} className="hover:text-green-500 mt-1" />
</button>
</form>
);
};
export default ProfileImageUpload;
리팩토링 하기 전이기도 하고... 그냥 코드 자체가 길고 복잡해보였다.
사전에 미리 이것저것 양식에 맞게 작성해줘야하는 것도 많은데 다 처음 보는 메소드고 방식이라 여기저기 찾아보면서 쓰는데도 이게 맞아?🤔 를 반복했다.
어려웠지만 사진 업로드도 잘 되고 전체적으로 기능구현에는 문제가 없었다. 문제는 버튼을 하나로 합치는 과정에서 나왔다.
지금은 수정한 모습이지만 원래는 이 모달창을 띄웠을 때 "저장"
"취소"
버튼과
위 코드에 있는 "이미지 저장"
버튼이 따로 있었다.
두 개의 버튼을 눌러야만 이미지 변경, 자기소개 변경까지 다 반영되는게 싫고 잘못된 방식👿이라고 생각했다. 그래서 모달창에 있는 저장버튼을 누르면 이미지 저장까지 다 되도록 수정하고자 했다.
// EditProfileModal.tsx
import { useEffect, useState } from "react";
import ProfileImageUpload from "./ProfileImageUpload";
interface EditProfileModalProps {
isOpen: boolean;
onClose: () => void;
userData: {
user_id: string;
user_nickname: string;
user_img: string;
user_introduce: string;
};
onImageUpload: (url: string) => void;
onIntroduceSave: (introduce: string) => void;
}
const EditProfileModal: React.FC<EditProfileModalProps> = ({
isOpen,
onClose,
userData,
onImageUpload,
onIntroduceSave
}) => {
const [editedIntroduce, setEditedIntroduce] = useState(userData.user_introduce);
useEffect(() => {
if (userData) setEditedIntroduce(userData.user_introduce);
}, [userData]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-gray-800 bg-opacity-50 flex justify-center items-center">
<div className="bg-white p-5 rounded-lg w-[400px]">
<h2 className="text-lg font-semibold mb-4">프로필 수정</h2>
<ProfileImageUpload userId={userData.user_id} onImageUpload={onImageUpload} />
<textarea
value={editedIntroduce}
onChange={(e) => setEditedIntroduce(e.target.value)}
className="w-full p-2 border rounded mt-3 resize-none focus:outline-none focus:border-orange-300"
placeholder="자기소개를 입력하세요"
/>
<div className="flex justify-end mt-4">
<button
onClick={() => {
onIntroduceSave(editedIntroduce);
onClose();
}}
className="p-2"
>
저장
</button>
<button onClick={onClose}>취소</button>
</div>
</div>
</div>
);
};
export default EditProfileModal;
모달은 이렇게 만들었었는데 onImageUpload
,onIntroduceSave
이 두 개를 onSave
로 묶어서 저장버튼에 넘겨줄 생각이었다.
간단할 줄 알았는데 이상하게 구현이 어그러졌다. 에러도 참 가지각색이었다.
코드 수정하면서 생겼던 문제들💥
걍 다 안 됐다. 수정을 하고 싶어도 Zod
로 작성한 코드를 작성하면서도 어려워했다보니 어디를 고치고 어디에 값을 넘겨주면 될지도 계속 이리저리 방황했다.
수정작업을 3번을 했는데도 계속 실패하고 하면 할수록 내 코드인데도 모르겠다 싶었다. 그래서 가만히 생각해봤다.
Zod
스택이 반드시 필요한 부분인가 -> No❌ 이미지 용량이나 확장자를 따질 필요가 없는 부분이다.🔥 결론! Zod
를 포기하고 간단하게 구현하자!
"use client";
import { useState } from "react";
interface ProfileImageUploadProps {
userId: string;
onImageUpload: (file: File | null) => void;
}
const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({ userId, onImageUpload }) => {
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const imageUrl = URL.createObjectURL(file);
setPreviewUrl(imageUrl);
onImageUpload(file);
}
};
return (
<div>
<input type="file" accept="image/*" onChange={handleFileChange} />
{previewUrl && <img src={previewUrl} alt="Image Preview" className="w-40 h-40 rounded-full mt-4 object-cover" />}
</div>
);
};
export default ProfileImageUpload;
Zod
로 작성했던 것보다 훨씬 짧아졌다.
특별한 라이브러리를 사용한건 없지만 코드를 짧게 썼다는 점은 마음에 든다.
"use client";
import { fetchUserProfile } from "@/serverActions/profileAction"; // 유저 정보 받아오기
import { uploadProfileImage } from "@/utils/uploadProfileImage"; // 이미지 업로드 함수 (수파베이스 로직)
import EditProfileModal from "./EditProfileModal"; // 모달창
interface UserProfile {
user_id: string;
user_nickname: string;
user_img: string;
user_email: string;
user_introduce: string;
}
const MyPageProfile = () => {
const [userData, setUserData] = useState<UserProfile | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadUserProfile = async () => {
const profileData = await fetchUserProfile();
if (profileData) {
setUserData(profileData); // 업데이트된 사용자 정보 설정
}
setLoading(false);
};
loadUserProfile();
}, []);
// 저장하기 버튼 -> 이미지, 자기소개 모두 저장
const handleSave = async (nickname: string, introduce: string, file: File | null) => {
if (!userData) return;
// 자기소개 업데이트
const { error: introError } = await supabase
.from("USER_TABLE")
.update({ user_nickname: nickname, user_introduce: introduce })
.eq("user_id", userData.user_id);
if (introError) {
console.error("자기소개 업데이트 오류:", introError.message);
return;
}
// 이미지 업로드 처리
if (file) {
const profileImageUrl = await uploadProfileImage(userData.user_id, file);
if (profileImageUrl) {
const { error: imgUpdateError } = await supabase
.from("USER_TABLE")
.update({ user_img: profileImageUrl })
.eq("user_id", userData.user_id);
if (imgUpdateError) {
console.error("USER_TABLE에 이미지 URL 업데이트 오류:", imgUpdateError.message);
return;
}
// 상태 업데이트
setUserData((prev) => (prev ? { ...prev, user_img: profileImageUrl } : null));
}
}
// 새로운 자기소개 반영
setUserData((prev) => (prev ? { ...prev, user_nickname: nickname, user_introduce: introduce } : null));
setIsModalOpen(false);
};
if (loading) return <p>Loading...</p>;
return (
{/* 프로필 수정 모달 */}
<EditProfileModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
userData={userData}
onSave={handleSave} // ✅ 버튼 하나로 모두 저장!
/>
);
};
export default MyPageProfile;
onSave
버튼 하나로 사진 변경, 자기소개 변경까지 모두 잘 구현되었다. 👏👏
이 코드가 자랑스럽고 100% 만족해서 작성한 글이 아니다.
여전히 반복된 로직도 많고 수정할 부분도 많고 초짜 코드이다. 하루종일 노력해서 기능 구현까지 완료한 코드를 내 손으로 다 지워야 할 때 받아들이기 힘들었다. 귀찮은 것도 있었고 시간은 없는데 계속 머물러서 똑같은 기능을 만드는것도 조바심났다. 여러 감정이 복합적으로 들어서 코드를 엎어야하는게 맞다..! 라고 생각한 순간 눈물이 왈칵 났다.😭😭 (프로젝트 하면서 생긴 부담감도 같이 터져버린 듯ㅋㅋ)
그냥 배운걸 써볼 수 있다는게 마냥 좋아서 그 기술 스택이 지금 기능에 필요한가? 를 생각하지 못한 것 같다. 그냥 유효성 검사를 해주고 튜터님들도 추천해서 좋은거니까 적용하면 다 좋을 줄 알았다.
이번 기회에 많이 배웠다. 내가 아빠옷을 입는다고 잘 맞고 잘 어울리고 편한게 아니듯이 기능에도 맞는 옷이 있다. 그 옷을 잘 골라주고 잘 입혀주는게 내 몫이다. 그 눈을 가지고 실력을 키워보자. 좋은 판단력을 길러보자.