<input type="file" />
으로 사진을 받는다.'Content-Type': 'application/json'
을 사용할 수 없다.formData (
request: {
title: string;
content: string;
options: [...];
:
},
contentImages: file;
optionImages: file[];
)
new File(['없는사진'], '없는사진.jpg'
으로 의미없는 파일을 만들어 넣어줬다. (넣지 않으면 undefined이 가는데, 이를 받지 못한다는 백엔드 요청으로 이렇게 처리됨)리팩토링 대상은 두 개였다.
new File(['없는사진'], '없는사진.jpg'
로 넣지 않기작성/수정 통신이 되지 않아 일단 데이터를 주고받는데 의의를 둔 명세를 수정했다.
// 수정 전
formData (
request: {
title: string;
content: string;
options: optionInfo[...];
:
},
contentImages: file;
optionImages: file[];
)
// 수정 후
formData(
categoryIds : number[],
title : string,
content : string,
imageUrl : "" | "imageUrl", //이 url은 이미지 미리보기를 위한 url
imageFile : file, //사용자가 등록한 이미지 파일
postOptions : [
{
content : string,
imageUrl : "" | "imageUrl",
imageFile : file,
},
...
],
deadline : "yyyy-mm-dd hh:mm"
)
수정 전 데이터는 글의 정보와 본문의 이미지 파일, 선택지의 이미지 파일로 크게 구분했다. 이보다는 연관성 높은 정보끼리 같은 곳에, 같은 뎁스에 있어야 한다고 생각을 해 아래와 같이 명세를 수정했다.
폼데이터 객체는 또 객체와는 작성법이 달라서 안에 객체 안 객체, 객체 안 배열을 넣을 수 있나 싶었는데 찾아보니 방법이 있었다.
//기본적으로 프론트에서 이렇게 넣어 서버에 보내면
formData.append(필드명: string, file | string)
//서버에서는 아래처럼 객체로 보입니다.
{
필드명: file | string
}
//이렇게 프론트에서 동일한 필드명을 넣어 보내면
formData.append(A, "Apple")
formData.append(A, "Banana")
//서버에서는 아래처럼 배열로 보입니다.
{
A: ["Apple", "Banana"]
}
//이렇게 프론트에서 "동일한 필드명[index].필드명"을 넣어 보내면
formData.append(A[0].a, "Apple")
formData.append(A[0].b, "Banana")
//서버에서는 아래처럼 객체가 들어있는 배열로 보입니다.
{
A: [{
a: "Apple",
b: "Banana"
}]
}
이에 따라 우리 코드를 아래와 같이 수정하였다.
const handlePostFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData();
// type="file"인 input을 배열형태로 가지고 오기
const imageFileInputs = e.target.querySelectorAll<HTMLInputElement>('input[type="file"]');
const fileInputList = [...imageFileInputs];
// 글의 카테고리/제목/내용/본문 imageUrl/마감일시 정보 넣기
(선택한 카테고리가 담긴 배열).forEach(categoryId =>
formData.append('categoryIds', categoryId.id.toString())
);
formData.append('title', writingTitle);
formData.append('content', writingContent);
formData.append('imageUrl', contentImage);
formData.append('deadline', addTimeToDate(time, baseTime));
// 선택지 id/내용/imageUrl이 포함된 객체의 배열 넣기
writingOptionList.forEach((option, index) => {
option.isServerId && formData.append(`postOptions[${index}].id`, option.id.toString());
formData.append(`postOptions[${index}].content`, deleteOverlappingNewLine(option.content));
formData.append(`postOptions[${index}].imageUrl`, option.imageUrl);
});
// 본문/선택지 이미지 파일들 넣기
fileInputList.forEach((item: HTMLInputElement, index: number) => {
if (!item.files) return;
// type="file"인 input을 배열형태로 모두 가지고 왔기 때문에 index로 본문인지 선택지인지 판별함
// 우리는 본문에 사진 1장, 각 선택지마다 사진 1장을 넣을 수 있음
// 때문에 "index === 0"는 본문에 들어가는 이미지 파일, 그 이후의 것은 선택지에 들어가는 이미지 파일.
if (index === 0) {
item.files[0] && formData.append('imageFile', item.files[0]);
} else {
item.files[0] && formData.append(`postOptions[${index - 1}].imageFile`, item.files[0]);
}
});
// 글 작성/수정을 요청하는 api와 연결된 리액트쿼리 함수
mutate(formData);
}
이제 보내게 되면
개발자도구 네트워크 탭 > preview 에서 위와 같이 확인할 수 있다.
카테고리 id
,제목
, 내용
, 본문 imageUrl
,
선택지 정보(내용/imageUrl)
, 마감시간
,
본문 image file
, 선택지 image file
.
선택지 이미지는 두 번째 선택지에만 넣었다.
이 부분은 서버에서 완전한 상태로 전달하게 되면서 프론트엔드에선 기존에 도메인을 붙였다가 삭제하던 부분을 수정하면 끝났다. 훨씬 덜 복잡하고, 덜 번잡해졌다.
이렇게 작성과 수정을 위한 폼 컴포넌트 리팩토링은 끝났다.
혹시 FormData를 사용하는 사람이 있다면 이 글을 통해 손쉽게 원하는 형태로 서버와 통신했으면 좋겠다.
사용자가 이미지를 업로드하면 다음과 같이 처리된다.
이 상태에서 이미지를 삭제하면 다음과 같이 처리된다.
ref.current.value = ''
처리 한다.때문에 게시글 작성의 경우라면 imageUrl이 있다면 file도 있고, imageUrl이 없다면 file도 없다.
하지만 게시글 수정의 경우에는 사용자가 기존에 작성한 글에 사진이 있다면 props로 전달되는 postData에 imageUrl은 있지만 file은 없게 된다.
때문에 수정의 경우 5가지 경우의 수가 생긴다.
경우의 수가 늘어나면서 이 로직을 백엔드와 완전히 이해하고 api를 맞추어 나가는 것이 어려웠다. 이해하고 나면 별 것 아닌데 말이다.
개발을 하다보면 늘 그런 것 같다.
할때는 모르겠고, 무슨 소린가 싶은데 이해하고 익숙해지면 별거 아니게 느껴진다.
백엔드와 이해를 동기화하고 동시에 작업을 진행, 머지하며 더 좋은 코드로 수정할 수 있어서 만족스럽다.
FormData에 대해 정리한 글을 보고 복습할 수 있어서 너무 좋았어요 - 우스 -