이미지 업로드 작업에서 너무 많은 시간을 써버렸네요..😅
이미지 업로드 문제를 해결하기 위해 React Hook Form을 정독했으니 팀 생성은 이제 아무것도 아닙니다❗❗
우선 팀 생성 API를 먼저 작업해 볼게요.
팀 생성 API
import axios from '@/app/lib/instance';
export interface PostGroupData {
profile?: string; // 프로필 이미지는 선택 사항
name: string;
}
const postGroup = async (data: PostGroupData) => {
const res = await axios.post('groups', data, {
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`,
},
});
return res;
};
export default postGroup;
- Authorization : 로그인이 아직 미완성이라 임의로 .env에 토큰을 저장해서 사용
Form onSubmit 핸들러
const onSubmit = async ({ profile, name }: FieldValues) => {
let imageUrl: string | null = null;
// 이미지 업로드 - 프로필 이미지 선택 안한 경우 생략
if (profile && profile[0] instanceof File) {
try {
const formData = new FormData();
formData.append('image', profile[0]);
const {
data: { url },
} = await postImage(formData);
imageUrl = url;
} catch (error) {
alert('이미지 업로드에 실패했습니다.');
}
}
// 팀 생성
try {
const teamData: PostGroupData = {
name,
};
if (imageUrl) {
teamData.profile = imageUrl;
}
await postGroup(teamData);
alert('팀이 성공적으로 생성되었습니다!');
} catch (error) {
alert('팀 생성에 실패했습니다.');
}
};
- profile && profile[0] instanceof File : profile(폼 이미지)이 존재하고 File 객체인지 확인
- if (imageUrl) {teamData.profile = imageUrl;} : 이미지를 선택한 경우만 teamData에 profile을 추가
결과를 확인해 봐야겠죠👀
이미지와 팀 이름 모두 정상적으로 적용되어 팀을 생성했네요👍
끝내기 전에 잠깐❗
page의 코드가 150줄이네요... 그다지 긴 것 같진 않지만 그래도 가독성과 재사용성을 고려해 분리해 볼게요✨
page.tsx
function Page() {
const router = useRouter();
const onSubmit = async ({ profile, name }: FieldValues) => {
/* 생략 */
};
return (
<div>
<div className="mx-auto mt-[3.75rem] max-w-[23.4375rem] px-4 pt-[4.5rem] tablet:w-[28.75rem] tablet:px-0 tablet:pt-[6.25rem]">
<h2 className="mb-6 text-center text-2xl font-medium text-text-primary tablet:mb-20">
팀 생성하기
</h2>
<TeamForm onSubmit={onSubmit}>생성하기</TeamForm>
<div className="mt-6 text-center text-md text-text-primary tablet:text-lg">
팀 이름은 회사명이나 모임 이름 등으로 설정하면 좋아요.
</div>
</div>
</div>
);
}
export default Page;
TeamForm.tsx
interface TeamFormProps {
onSubmit: (data: FieldValues) => Promise<void>;
}
function TeamForm({ children, onSubmit }: PropsWithChildren<TeamFormProps>) {
const method = useForm();
const { register, handleSubmit } = method;
return (
<FormProvider {...method}>
<form className="flex flex-col gap-6" onSubmit={handleSubmit(onSubmit)}>
<ProfileUploader register={register} />
<Input
name="name"
title="팀 이름"
type="text"
placeholder="팀 이름을 입력해주세요."
validationRules={{
required: '이름을 입력해주세요.',
minLength: {
value: 1,
message: '이름은 최소 1글자 이상입니다.',
},
maxLength: {
value: 30,
message: '이름은 최대 30글자까지 입력 가능합니다.',
},
validate: (value) =>
value.trim() !== '' || '팀 이름에 공백만 입력할 수 없습니다.',
}}
autoComplete="off"
/>
</form>
<Button
variant="primary"
className="mt-10 w-full text-white"
onClick={handleSubmit(onSubmit)}
>
{children}
</Button>
</FormProvider>
);
}
ProfileUploader
function ProfileUploader({ register }: ProfileUploaderProps) {
const [profileImage, setProfileImage] = useState('');
// 파일 처리하는 함수
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file); // 미리보기 URL 생성
setProfileImage(url); // 미리보기 이미지 업데이트
}
};
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>
);
}
이대로 마무리되면 좋겠지만... 배포 단계에서 에러가 발생했습니다😭😭
에러 메시지를 보니 postIamge의 response 타입이 제대로 정의되지 않은 것 같아요.
함수의 return 타입을 정의해줄게요😀
type PostImageResponse = {
url: string;
};
const postImage = async (img: FormData): Promise<PostImageResponse> => {
const res = await axios.post('images/upload', img, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`,
},
});
return res.data;
}
type PostGroupResponse = {
id: string;
};
const postGroup = async (data: PostGroupData): Promise<PostGroupResponse> => {
const res = await axios.post('groups', data, {
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`,
},
});
return res.data;
}
결과는
👀
👀
👀
👀
아직도 에러가 발생하네요😭😭😭
이번에는 에러 메시지가 바뀐 것 같아요. axios 요청의 응답이 unknown 타입이라 PostGroupResponse 타입에 할당할 수 없다고 하네요.
흠.. 함수의 반환 타입으로 Promise<PostGroupResponse>라는 비동기 타입을 반환하는 것으로 해결이 안 되는 것 같네요.
그럼 적용해 볼 수 있는 방법은 axios 요청의 response 타입을 직접 정의해 주는 것뿐이네요🤔🤔🤔
type PostGroupResponse = {
id: string;
};
const postGroup = async (data: PostGroupData): Promise<PostGroupResponse> => {
const res = await axios.post<PostGroupResponse>('groups', data, {
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`,
},
});
return res.data;
};
const postImage = async (img: FormData): Promise<PostImageResponse> => {
const res = await axios.post<PostImageResponse>('images/upload', img, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`,
},
});
return res.data;
};
이전 프로젝트에서는 axios 요청의 response 타입을 <T> 형태로 명시하지 않아도 에러가 발생하지 않아서 잘 몰랐네요.
함수의 반환 타입에 Promise<T> 형태만 지정해 주면 알아서 타입을 추론한다고 생각했는데, 지금까지 any 타입으로 추론해서 에러가 발생 안 했던 거라니😅😅
이전에는 런타임에서만 오류를 신경 썼기 때문에 빌드 타임에서 발생하는 오류에 대해서는 잘 몰랐어요.
그런데 이번에 타입을 명시하지 않아 빌드 타임에서 오류가 발생하고, 해결을 위해 많은 시간을 투자하고 보니 타입을 명확하게 지정하는 것이 얼마나 중요한지 확실히 느꼈습니다.
추가로 배포 주기를 짧게 잡는 것도 좋은 방법이라는 생각이 들었어요.
배포 주기를 짧게 잡으면 문제를 빠르게 발견하고 수정하는 것이 가능하니, 문제가 커지기 전에 발견하고 대응할 수 있어 안정적이고 효율적인 개발이 가능할 것 같아요❗❗❗❗
'프로젝트 > Next+TypeScript' 카테고리의 다른 글
[Coworkers] 클립보드 복사 (Trouble Shooting) (0) | 2025.01.27 |
---|---|
[맛길] API Routes를 이용한 API 구축 (2) | 2025.01.24 |
[Coworkers] 팀 생성 페이지 작업 (API 연동) (0) | 2025.01.22 |
[Coworkers] Trouble Shooting - 코드 리뷰 (0) | 2025.01.21 |
[Coworkers] 공통 컴포넌트 Modal (0) | 2025.01.20 |