본문 바로가기
✨FRONTEND/📍Next.js

프로필 이미지 업로더 구현하기(feat. 이미지 미리보기)

by 짱돌보리 2025. 5. 10.
728x90

프로필 이미지 업로더 구현하기(feat. 이미지 미리보기)📸

🚀구현 목표

- 사용자가 프로필 이미지를 클릭하면 업로드 가능
- 이미지를 선택하면 즉시 미리보기로 반영
- 편집 모드일 때만 업로드 가능 (일반 모드에서는 그냥 이미지 보여주기)

ProfileImageUploader 컴포넌트 만들기

먼저 이미지 업로드 기능을 담당하는 컴포넌트를 만들었음!

 

1️⃣ 초기 상태 (기본 이미지 출력)
useState로 초기 미리보기 이미지 (preview) 를 설정함!

const [preview, setPreview] = useState(profileImageUrl || profilePlaceholder)

 

2️⃣ 파일 선택 (input 이벤트 발생)
파일 업로드 버튼을 누르면 handleImageChange 함수가 실행됨!

const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0] // 사용자가 선택한 파일 가져오기
  if (file) {
    setPreview(URL.createObjectURL(file)) // 미리보기용 URL 생성
    onImageChange(file) // 부모 컴포넌트에 전달
  }
}

 

3️⃣ 미리보기 이미지 변경
파일 선택 후, 선택한 이미지가 즉시 보임!

<div className="relative">
  <Image
    src={preview}
    alt="프로필 이미지"
    className="size-30 rounded-full object-cover md:size-40"
    width={120}
    height={120}
  />
  <input
    type="file"
    accept="image/*"
    onChange={handleImageChange}
    className="absolute inset-0 cursor-pointer opacity-0"
  />
</div>
  • inset-0: top: 0; right: 0; bottom: 0; left: 0; → 부모 영역을 꽉 채움
  • opacity-0: opacity: 0; → 완전히 투명하게 만들어서 보이지 않음

→ input[type="file"]는 보이지 않지만, div를 클릭하면 파일 업로드 창이 뜨도록 동작함!
→ 사용자는 이미지를 클릭하는 것처럼 느끼지만, 사실 보이지 않는 input을 클릭하는 거임

 

풀코드)

import { ChangeEvent, useState } from 'react'
import Image, { StaticImageData } from 'next/image'
import profilePlaceholder from '@/assets/icons/profile.png'

type ProfileImageUploaderProps = {
  profileImageUrl: string | StaticImageData
  onImageChange: (file: File | null) => void
}

export default function ProfileImageUploader({
  profileImageUrl,
  onImageChange,
}: ProfileImageUploaderProps) {
  const [preview, setPreview] = useState(profileImageUrl || profilePlaceholder)

  const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (file) {
      setPreview(URL.createObjectURL(file)) // 이미지 미리보기
      onImageChange(file) // 상위 컴포넌트에 파일 전달
    }
  }

  return (
    <div className="relative">
      <Image
        src={preview}
        alt="프로필 이미지"
        className="size-30 rounded-full object-cover md:size-40"
        width={120}
        height={120}
      />
      <input
        type="file"
        accept="image/*"
        onChange={handleImageChange}
        className="absolute inset-0 cursor-pointer opacity-0"
      />
    </div>
  )
}

ProfileImageUploader를 상위컴포넌트에 넣어주자

  • isEditing이 true일 때만 업로더 보여줌
  • imagePreviewUrl: 사용자가 새로 선택한 이미지의 미리보기 URL
  • profileEdit: 편집 모드일 때 보여줄 기본 이미지
  • setSelectedImage: 이미지 선택 시 File 객체 저장하는 함수
const [isEditing, setIsEditing] = useState(false)
const [selectedImage, setSelectedImage] = useState<File | null>(null)
const [imagePreviewUrl, setImagePreviewUrl] = useState<string>('')

// 업로드 시 미리보기 URL 저장
const handleImageChange = (file: File | null) => {
  if (file) {
    setSelectedImage(file)
    setImagePreviewUrl(URL.createObjectURL(file))
  }
}
<div className="rounded-full border-6 md:border-8">
  {isEditing ? (
    <ProfileImageUploader
      profileImageUrl={imagePreviewUrl || profileEdit}
      onImageChange={setSelectedImage}
    />
  ) : (
    <Image
      src={user?.profileImageUrl || profile}
      alt="프로필 이미지"
      className="size-30 rounded-full object-cover transition-all md:size-40"
      width={120}
      height={120}
    />
  )}
</div>

 

구현 완!🥳🥳🥳

변경 전)

 

편집 중)

 

변경 후)