최종 프로젝트 - 사진 수정 및 업로드 구현하면서 느낀점

하영·2024년 10월 31일
1

팀프로젝트

목록 보기
20/27

트러블슈팅인가..? 그건 아니지만 느낀점이 많았던 작업이라 TIL로 남겨보기로 했다.
마이페이지에 사진이나 개인정보를 수정하는 기능을 구현해야 했는데 간단한 부분이지만 참 고생길을 걸었다...🫠

사진 수정 업로드 구현하기

01. Zod 를 사용해보자!

본격적으로 팀프로젝트 기능구현하기 전에 React-hook-form 이랑 Zod를 써보기로 했었다. supabase도 어려운데 강의로만 공부하던 걸 적용하기 어려울 것 같아서 미리 공부도 간단하게 했었다.

이걸 꼭 써보고 싶어서 인증/인가 부분을 하고 싶었는데 아쉽게도 그 부분은 다른 팀원이 맡게 되었다.
그러다가 이미지 수정 쪽에 자기소개랑 닉네임 수정 폼이 있어서 여기에 써볼 수 있지 않을까? 해서 좀 신이 났었다!! 공부한 걸 써볼 수 있겠구나!!

아무튼 그렇게 작성한 코드는 이랬다.


Zod 로 구현한 코드 👩🏻‍💻

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;

리팩토링 하기 전이기도 하고... 그냥 코드 자체가 길고 복잡해보였다.
사전에 미리 이것저것 양식에 맞게 작성해줘야하는 것도 많은데 다 처음 보는 메소드고 방식이라 여기저기 찾아보면서 쓰는데도 이게 맞아?🤔 를 반복했다.

어려웠지만 사진 업로드도 잘 되고 전체적으로 기능구현에는 문제가 없었다. 문제는 버튼을 하나로 합치는 과정에서 나왔다.

지금은 수정한 모습이지만 원래는 이 모달창을 띄웠을 때 "저장" "취소" 버튼과
위 코드에 있는 "이미지 저장" 버튼이 따로 있었다.

두 개의 버튼을 눌러야만 이미지 변경, 자기소개 변경까지 다 반영되는게 싫고 잘못된 방식👿이라고 생각했다. 그래서 모달창에 있는 저장버튼을 누르면 이미지 저장까지 다 되도록 수정하고자 했다.


02. 꼬일대로 꼬인 코드들 🤯

// 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 로 묶어서 저장버튼에 넘겨줄 생각이었다.
간단할 줄 알았는데 이상하게 구현이 어그러졌다. 에러도 참 가지각색이었다.

코드 수정하면서 생겼던 문제들💥

  1. 콘솔에 바꾼 이미지가 찍히는데 업로드가 안 됨
  2. 패치가 안 됨
  3. 새로고침하면 수파베이스에 저장이 안 됨

걍 다 안 됐다. 수정을 하고 싶어도 Zod로 작성한 코드를 작성하면서도 어려워했다보니 어디를 고치고 어디에 값을 넘겨주면 될지도 계속 이리저리 방황했다.

03. 꼭 Zod가 필요한 상황인가?

수정작업을 3번을 했는데도 계속 실패하고 하면 할수록 내 코드인데도 모르겠다 싶었다. 그래서 가만히 생각해봤다.

  1. Zod 스택이 반드시 필요한 부분인가 -> No❌ 이미지 용량이나 확장자를 따질 필요가 없는 부분이다.
  2. 현재 코드를 전부 이해하고 남들이 물어봤을 때 설명할 수 있는가 -> No❌ 저 코드들을 100% 이해하고 쓰지도 못했고 분명 리펙토링 할 때 까막눈으로 코드를 볼 것 같았다.
  3. 지금 코드에 만족하는가 -> No❌ 일단 코드가 너무 길어서 보기 싫다는 생각이 들고 그냥 복잡해보이는 것 같았다.

🔥 결론! Zod를 포기하고 간단하게 구현하자!


04. 코드를 엎어봅시다~!

ProfileImageUpload - 사진 업로드

"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로 작성했던 것보다 훨씬 짧아졌다.
특별한 라이브러리를 사용한건 없지만 코드를 짧게 썼다는 점은 마음에 든다.

MyPageProfile - 프로필 보여주기

"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 버튼 하나로 사진 변경, 자기소개 변경까지 모두 잘 구현되었다. 👏👏


05. 느낀점 🥹

코드가 자랑스럽고 100% 만족해서 작성한 글이 아니다.
여전히 반복된 로직도 많고 수정할 부분도 많고 초짜 코드이다. 하루종일 노력해서 기능 구현까지 완료한 코드를 내 손으로 다 지워야 할 때 받아들이기 힘들었다. 귀찮은 것도 있었고 시간은 없는데 계속 머물러서 똑같은 기능을 만드는것도 조바심났다. 여러 감정이 복합적으로 들어서 코드를 엎어야하는게 맞다..! 라고 생각한 순간 눈물이 왈칵 났다.😭😭 (프로젝트 하면서 생긴 부담감도 같이 터져버린 듯ㅋㅋ)

그냥 배운걸 써볼 수 있다는게 마냥 좋아서 그 기술 스택이 지금 기능에 필요한가? 를 생각하지 못한 것 같다. 그냥 유효성 검사를 해주고 튜터님들도 추천해서 좋은거니까 적용하면 다 좋을 줄 알았다.

이번 기회에 많이 배웠다. 내가 아빠옷을 입는다고 잘 맞고 잘 어울리고 편한게 아니듯이 기능에도 맞는 옷이 있다. 그 옷을 잘 골라주고 잘 입혀주는게 내 몫이다. 그 눈을 가지고 실력을 키워보자. 좋은 판단력을 길러보자.

profile
왕쪼랩 탈출 목표자의 코딩 공부기록

0개의 댓글

관련 채용 정보