며칠 전 마무리한 팀프로젝트 과정중에 겪었던 문제 상황을 정리해보면서 우리팀의 해결 방식을 소개해 보겠다.
팀프로젝트는 칵테일 레시피 공유 서비스였는데, 이를 개발하던 도중 팀원분이 담당하신 레시피 등록 기능 구현에 관해 도움을 요청 받아 같이 고민하게 되었다.
이미지 업로드에 관해서는 사전 회의에서 백엔드에 formData로 주기로 결정을 했다. 그렇게 알고 진행되던 중 백엔드분의 질문에 부족한 사전조사의 폐해가 들어났다.
이미지랑 레시피랑 "Content-Type"을 다르게 해서 보내주실 수 있나요?
당시 등록 기능을 담당한 프론트 팀원분이 가능할 거 같다는 답변과 함께 방법을 찾아오겠다 말씀하셨다. 그렇게 다음날이 되었을 때 등록 요청 테스트 실패와 함께 문제 해결을 위한 고민이 시작되었다.
한번의 요청에 복수의 content-type 명시가 가능한지를 찾아다녔지만, 결론은 불가능하고 복수의 content-type을 명시해도 하나만 적용된다는 답을 얻게 되었다.
'한번에 안된다면 나눠서 보내면 되겠다'는 라는 아이디어를 통해 백엔드에서는 api를 쪼개고, json 타입을 받는 api에서 응답으로 id값을 보내주기로 했다
const [id, setId] = useState<number>()
const recipe = //칵테일 레시피 정보
const formData = // 이미지를 가지는 formData 객체
axios.post("http://domain.com", recipe) // 첫번째 요청 : 레시피
.then(res=> setId(res.data.id)) // 응답 레시피 id
axios.post(`http://domain.com/${id}`, formData) // 받아온 id를 통해 레시피에 맞는 이미지 등록
.then(res=> console.log("레시피 등록 성공"))
// react query
recipeMutation.mutate(customRecipeCreateDto, {
onSuccess: (id) => { // json 데이터 등록 성공시 레피시 id를 응답으로 받는다
const formData = new FormData();
formData.append("image", selectedImage);
const input = {
id: id,
formData: formData,
};
imageMutation.mutate(input, {
onSuccess: () => {
navigate("/custom"); // 성공시 사용자 레시피 목록으로 이동
},
});
},
});
이런식으로 백엔드와 함께 api를 쪼개서 요청을 두번 보내는 것으로 해결.. 했으면 좋았겠지만 에러 처리가 남았었다.
우리의 행복회로는 이러했다.
하지만 현실은 녹록치 않았다.
recipe가 등록되면 db에 저장되고 해당 레시피의 id를 받아와 추가적으로 이미지를 업로드하는 방식이기 때문에 이미지 등록 요청이 실패하면 이미지가 없는 레시피 카드가 생겨나게 된다.
이미지 등록에 실패하면 기존 레시피등록을 취소할 수 있으면 좋을 텐데 어떤 방법이 있을까
그래서 우리는 이미지 등록이 실패 했을때 첫 번째 요청에 의해 만들어진 레시피를 삭제하는 방법을 선택했다.
이제 백엔드한테 이 로직을 만들어 달라고 하자!! 라고 말하기에는 너무 무책임하다는 생각이 들었기에 기존 코드를 유지하면서 해결할 수 있는 방법을 생각해 보았다.
생각해보니 우리는 이미 레시피 삭제 기능을 만들어 놨었기에 레시피 삭제 요청을 등록 로직에 추가하기로 했다.
우리 프로젝트는 react query를 통해 데이터를 관리하고 있었기에 mutation의 onError 옵션으로 delete 처리를 진행 했다.
recipeMutation.mutate(customRecipeCreateDto, {
onSuccess: (id) => {
const formData: FormData = new FormData();
formData.append("image", selectedImage);
const input = {
id: id,
formData: formData,
};
imageMutation.mutate(input, {
onSuccess: () => {
navigate("/custom"); // 성공시 커스텀 레시피 목록으로 이동
},
onError: () => {
window.alert("이미지 등록실패");
deleteMutation.mutateAsync(id);
},
});
},
});
이러한 방식으로 우리는 두가지 타입의 등록기능을 처리 했다. 무의식적으로 한번의 요청만으로 하나의 기능을 완성해야 한다는 생각이 해결을 늦추게 된 문제 케이스였다.
막연하게 그냥 만들면 되겠지는 혼자서 할때에는 큰 문제가 안되지만 여러 사람이 같이 작업할때는 생각보다 걸림돌이 된다는 것을 깨달았다. 이번 문제 대응을 통해서 기능 구현 전에 어떠한 것들이 필요할지 계획을 잘 짜두는 것이 원할한 개발에 많은 영향을 끼친다는 것을 알게 되는 좋은 계기였다.
조금 시간이 지난 후 레시피 수정 쪽에서 문제가 생겼다는 소식을 접했다.
당연하게도 이미지 등록 기능이 문제였다.
레시피 등록 로직을 그대로 수정에 적용해 놓은게 화근이었다.
이미지 수정 요청은 이미지가 null이면 오류 코드 없이 기존 이미지를 사용하는 것으로 처리하기로 했으나 최초 구현 이후 며칠이 지나고 갑자기 오류가 발생하기 시작했다.
const { data: recipeId } = await recipeMutation.mutateAsync(
customRecipeCreateDto,
);
const formData = new FormData();
formData.append("image", selectedImage || null);
const imageInput = {
id: recipeId,
formData: formData,
};
await imageMutation.mutateAsync(imageInput);
문제의 원인은 이미지가 null이 아니라 undefined로 보내지는 것이었다.
프론트에서 null로 명시해 보내도 백엔드가 받는 것은 undefined가 되어서 오류가 생겼다.
사실 기존 로직은 undefined로 보내지는 점 말고 근본적인 문제가 존재했다.
이미지가 교체되었건 교체되지 않았건 무조건적으로 이미지 patch 요청이 들어간다는 것이었다.

이 근본적인 문제를 해결하면 undefined관련 오류는 해결되는 것과 마찬가지 이기 때문에 기존 로직을 아래와 같이 수정했다
이미지가 교체 되었을 때만 이미지 patch 요청이 들어간다
await recipeMutation.mutateAsync(customRecipeCreateDto);
if (selectedImage) { // 이미지가 교체되지 않으면 초기값인 undefined
const formData = new FormData();
formData.append("image", selectedImage);
const imageInput = {
id: id,
formData: formData,
};
await imageMutation.mutateAsync(imageInput);
}
navigate("/custom");
이렇게 프로젝트 기간동안 이미지 관련 오류를 접했고, 수정하게 되었다.
복수의 타입을 다루는 요청에서 고려해야 할 점은 처리 과정을 억지로 한번의 과정으로 엮어서 처리 하려고 하면 안된다는 것이다.
요청을 여러개로 쪼개야 하는 상황이라면 다시 처음부터 요구사항을 다시 그려보고 그에 맞는 액션을 취하도록 코드를 재구성 해봐야 한다. 위의 두 사례 모두 두개의 요청을 하나의 기능이라고 생각 했기 때문에 발생한 문제였기 때문이다.
이번 프로젝트에서는 typescript를 적용해 진행을 하였는데 typescript덕분에 수월하게 문제점을 찾은것 같아서 왜 사람들이 typescript를 많이 사용하고 있는지 조금은 공감하게 된것 같다.
javascript만을 사용해서 진행 했다면 다른 사람의 코드에서 문제점을 찾아내는데 더 많은 시간이 걸렸을 것이다.