본문 바로가기

study/100 days (100일 챌린지)

[웹개발 100일] Day 45~46 - 클라이언트의 이미지 업로드 요청 처리해서 Cloudinary 라이브러리에 저장하기

반응형

 

 

 

프로필 사진 변경 기능을 위해 클라이언트~클라우디너리 라이브러리까지 이미지를 보내는 로직을 구현해봤어요.

 

 


🚀 요약

작업 시간: 3시간
✅ 프로필 사진 변경 기능 구현

 

 


🚀 프로필 사진 변경 기능 구현

 

 

로직 흐름은 이렇습니다.

 

1. 사용자가 파일 선택

2. 임시 url을 만들어 preview 보여줌

3. 사용자가 저장 버튼 클릭 시 이미지를 Cloudinary에 업로드

4. 업로드 후 반환된 url을 DB에 업데이트

 

사용자가 파일을 선택할 때마다 바로바로 클라우드에 업로드 하면 최종 선택한 이미지를 제외하고 모두 올펀 에셋 (orphan essets)이 됩니다. 때문에 저장 버튼 클릭 이후에 클라우드로 전송하는 게 데이터 관리 측면에서 효율적일 수 있겠죠.

 

 


 

 

프로필 이미지 프리뷰를 보여주는 컴포넌트

import { useRef } from "react";

interface ProfileUploaderProps {
  previewUrl: string | null;
  onFileSelect: (file: File) => void;
}

const ProfileUploader = ({
  previewUrl,
  onFileSelect,
}: ProfileUploaderProps) => {
  const inputRef = useRef<HTMLInputElement | null>(null);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) onFileSelect(file);
  };

  return (
    <div className="w-full flex flex-col items-center justify-center gap-2">
      <div
        className="relative w-[10rem] h-[10rem] m-1 rounded-full overflow-hidden cursor-pointer group"
        onClick={() => inputRef.current?.click()}
      >
        <img
          className="w-full h-full object-cover"
          src={previewUrl || "/default.png"}
          alt="profile"
        />
        <div className="absolute inset-0 hidden group-hover:flex items-center justify-center bg-gray-800 bg-opacity-60">
          <img
            className="w-10 h-10 filter invert"
            src="/images/icons/edit.png"
            alt="edit"
          />
        </div>
      </div>
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        className="hidden"
        onChange={handleFileChange}
      />
    </div>
  );
};

export default ProfileUploader;

 

 

상위 프로필 컴포넌트에서 호출

저장 버튼 클릭 시, 클라우드 업로드와 DB 업데이트를 실행합니다.

<ProfileUploader
    previewUrl={
    selectedFile ? URL.createObjectURL(selectedFile) : profileImg
    }
    onFileSelect={setSelectedFile}
/>

<button
    className={
      (isPwdValid && isPwdConfirmed && isUsernameValid
        ? "bg-blue-600 border-blue-600 "
        : "bg-gray-300 border-gray-300 ") +
      "text-white m-1 py-2 px-4 border-2 rounded-md w-[20rem]"
    }
    disabled={!(isPwdValid && isPwdConfirmed && isUsernameValid)}
    onClick={handleSaveClick}
    >
    Save Changes
</button>
const handleSaveClick = async () => {
    let uploadedUrl = profileImg;

    if (selectedFile) {
      const formData = new FormData();
      formData.append("file", selectedFile);
      formData.append("upload_preset", UPLOAD_PRESET);

      try {
        const res = await fetch(
          `https://api.cloudinary.com/v1_1/${CLOUD_NAME}/image/upload`,
          {
            method: "POST",
            body: formData,
          }
        );

        const data = await res.json();
        uploadedUrl = data.secure_url;
      } catch (err) {
        setMsg("이미지 업로드에 실패했습니다.");
        return;
      }
    }

    axios
      .put(
        "http://localhost:5232/api/users/update",
        {
          Email: email,
          PasswordHash: password,
          Username: username,
          ProfileImageUrl: uploadedUrl,
        },
        { withCredentials: true }
      )
      .then((response) => {
        if (response.data.message) {
          setMsg(response.data.message);
          setProfileImg(uploadedUrl); // 업로드 성공 시 이미지 적용
        } else {
          setMsg("Failed to update the profile.");
        }
      })
      .catch((err) => {
        setMsg("프로필 저장에 실패했습니다.");
      });
};

 

 

오늘 작업 끝!

다음으로는 프로필 페이지 디테일을 좀 챙긴 후에, 대망의 게시글 업로드.. 페이지에 손을 대보려고 합니다.

 

 

 

반응형