프로젝트/Next+TypeScript

[Coworkers] IOS 이미지 업로드 이슈

dev-hpk 2025. 2. 12. 00:39

🚨 문제 상황

팀 수정하기 관련해서 IOS 기기에서 이미지가 업로드되지 않는 이슈가 생겼습니다.

프로필 이미지 이슈
배포 이슈

이미지 업로드 관련 코드

uploadImage.ts

import postImage from '@/app/lib/image/postImage';

const uploadImage = async (profile: FileList) => {
  if (!profile || !(profile[0] instanceof File)) return null;

  const formData = new FormData();
  formData.append('image', profile[0]);

  const { url } = await postImage(formData);
  return url;
};

export default uploadImage;

 

위 코드는 react-hook-form의 profile 이미지를 받아서 서버에 업로드하는 함수입니다.

pc, 노트북, 갤럭시 핸드폰으로 확인했을 때 모두 정상적으로 동작합니다. safari 브라우저 문제인가 싶어 chrome으로도 확인했지만, ios에서는 uploadImage 함수가 정상적으로 동작하지 않습니다.

 

ios 기기로 매개변수 profile을 확인하기 위해 디버깅용 alert 코드를 추가해 보겠습니다.

import postImage from '@/app/lib/image/postImage';

const uploadImage = async (profile: FileList) => {
  alert(profile);
  alert(profile[0]);
  if (!profile || !(profile[0] instanceof File)) return null;

  const formData = new FormData();
  formData.append('image', profile[0]);

  const { url } = await postImage(formData);
  return url;
};

export default uploadImage;

 

아래 같은 결과가 나옵니다.

[object FileList]
undefined

 

profile[0]이 undefined인 것을 보니 uploadImage 함수 문제가 아닌 것 같습니다. 프로필 이미지를 변경하는 input 컴포넌트를 확인해 보겠습니다.

 

ProfileUploader.tsx

'use client';

import Image from 'next/image';
import { useEffect, useState } from 'react';
import { FieldValues, UseFormRegister } from 'react-hook-form';
import IconProfileEdit from '@/app/components/icons/IconProfileEdit';
import IconProfile from '@/app/components/icons/IconProfile';

interface ProfileUploaderProps {
  initialImage?: string;
  register: UseFormRegister<FieldValues>;
}

function ProfileUploader({ initialImage, register }: ProfileUploaderProps) {
  const [profileImage, setProfileImage] = useState<string | null>(
    initialImage || '',
  );

  // 파일 처리하는 함수
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const url = URL.createObjectURL(file); // 미리보기 URL 생성
      setProfileImage(url); // 미리보기 이미지 업데이트
    }
  };

  useEffect(() => {
    setProfileImage(initialImage || '');
  }, [initialImage]);

  return (
    <div>
      <span className="mb-3 inline-block">팀 프로필</span>
      <label
        htmlFor="profile"
        className="relative block h-16 w-16 cursor-pointer"
      >
        <input
          id="profile"
          className="sr-only"
          type="file"
          accept="image/*"
          {...register('profile')}
          onChange={handleFileChange}
        />
        {profileImage ? (
          <>
            <Image
              src={profileImage}
              className="rounded-full border-2 border-border-primary"
              fill
              alt="프로필 이미지"
            />
            <IconProfileEdit className="absolute bottom-0 right-0" />
          </>
        ) : (
          <IconProfile />
        )}
      </label>
    </div>
  );
}

export default ProfileUploader;

 

현재 코드는  onChange 핸들러와 register를 모두 처리하고 있습니다.

react-hook-form 공식 문서의 register를 확인해 보니 {...register('name')}이 onChange, onBlur, name, ref를 모두 처리하고 있다고 합니다. 

react-hook-form register 설명

 

이미지를 변경했을 때 미리보기 이미지도 변경해줘야 하니 register를 제거하는 방향으로 수정하겠습니다.

 

ProfileUploader.tsx - 수정 부분

'use client';

import Image from 'next/image';
import { useEffect, useState } from 'react';
import { FieldValues, UseFormSetValue } from 'react-hook-form';
import IconProfileEdit from '@/app/components/icons/IconProfileEdit';
import IconProfile from '@/app/components/icons/IconProfile';

interface ProfileUploaderProps {
  initialImage?: string;
  setValue: UseFormSetValue<FieldValues>;
}

function ProfileUploader({ initialImage, setValue }: ProfileUploaderProps) {
  const [profileImage, setProfileImage] = useState<string | null>(
    initialImage || '',
  );

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      const file = e.target.files[0];
      const url = URL.createObjectURL(file); // 미리보기 URL 생성
      setProfileImage(url);
      setValue('profile', file);
    }
  };

  useEffect(() => {
    setProfileImage(initialImage || '');
  }, [initialImage]);

  return (
    <div>
      <span className="mb-3 inline-block">팀 프로필</span>
      <label
        htmlFor="profile"
        className="relative block h-16 w-16 cursor-pointer"
      >
        <input
          id="profile"
          className="sr-only"
          type="file"
          accept="image/*"
          onChange={handleFileChange}
        />
        {profileImage ? (
          <>
            <Image
              src={profileImage}
              className="rounded-full border-2 border-border-primary"
              fill
              alt="프로필 이미지"
            />
            <IconProfileEdit className="absolute bottom-0 right-0" />
          </>
        ) : (
          <IconProfile />
        )}
      </label>
    </div>
  );
}

export default ProfileUploader;
  • setValue : 프로필 이미지 input 변경 시 react-hook-form의 fieldValue를 직접 수정하기 위해 props로 추가
  • setValue('profile', file) : onChange 이벤트 발생 시 파일을 fieldValue로 설정
  • input : register 옵션 삭제

결과를 확인해 보니 이제는 ios뿐 아니라 모든 기기에서 이미지 업로드가 동작하지 않습니다. 매개변수 profile이 잘 전달되고 있는지 확인하기 위해 디버깅용 alert 코드를 추가해 보겠습니다.

import postImage from '@/app/lib/image/postImage';

const uploadImage = async (profile: FileList) => {
  alert(profile);
  if (!profile || !(profile[0] instanceof File)) return null;

  const formData = new FormData();
  formData.append('image', profile[0]);

  const { url } = await postImage(formData);
  return url;
};

export default uploadImage;
[object File]

 

왜 File이 출력되는지 확인하기 위해 handleFileChange 함수를 확인해 보고 이유를 알았습니다.

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.files && e.target.files.length > 0) {
    const file = e.target.files[0];
    const url = URL.createObjectURL(file); // 미리보기 URL 생성
    setProfileImage(url);
    setValue('profile', file);
  }
};

 

setValue('profile', file)profile fieldValue에 files[0]인 파일을 저장했습니다.

 

uploadImage.ts - 매개변수 타입 수정

import postImage from '@/app/lib/image/postImage';

const uploadImage = async (profile: File) => {
  if (!profile || !(profile instanceof File)) return null;

  const formData = new FormData();
  formData.append('image', profile);

  const { url } = await postImage(formData);
  return url;
};

export default uploadImage;

 

매개변수 profile의 타입을 File로 수정 후 결과를 확인하니 정상적으로 잘 동작합니다.

 

이미지 업로드 이슈 수정하면서 느낀 점

ProfileUploader 컴포넌트에서 이미지 업로드가 iOS에서 동작하지 않는 문제가 발생했습니다. 처음에는 로직 자체에 문제가 있다고 생각했지만, 디버깅을 통해 react-hook-form의 register 사용법을 정확히 숙지하지 않았던 것이 원인임을 알게 되었습니다. 

 

이슈를 해결하면서 2가지 중요성을 깨달았습니다.

 

1️⃣ 공식 문서 분석의 중요성

공식 문서를 확인해 보니, register를 사용할 경우 이벤트 처리는 내부적으로 관리되므로 onChange를 따로 지정하면 예상치 못한 동작이 발생할 수 있다는 내용이 있었는데 놓쳤습니다. 앞으로는 공식 문서를 먼저 확인하고 정확한 사용법을 숙지한 후 적용해야겠습니다.

2️⃣ 디버깅의 중요성

alert(profile), alert(profile[0]) 등의 과정을 거치면서 디버깅을 통해 문제의 원인을 단계적으로 추적하는 것이 얼마나 중요한지 다시금 실감했습니다. 단순히 코드가 "안 된다"가 아니라 디버깅을 통해 어느 부분에서 왜 예상과 다르게 동작하는지를 분석하는 습관을 더욱 길러야겠습니다.

 

추가로 이번 경험을 통해 단순히 코드 문제를 해결하는 것뿐만 아니라 같은 실수를 반복하지 않도록 학습하고 기록하는 것이 얼마나 중요한지 깨달았습니다. 다음번에 또 같은 문제를 마주한다면 이번 기록을 통해 더 빠르고 효율적으로 해결할 수 있을 것 같습니다!