
useQuery와 useQueries에 대해 쓰기도 전에 어쩌다보니 useMutation 작성 뻘짓 일기부터 쓰게 되었다😓
하지만 이번에 안 쓰면 정말 다 까먹을 것 같았는걸…
Fetch 조건
Form 제출을 위한 API 호출 단계로, 내용에 대한 부분과 입력 완료에 대한 부분이 나뉘어져 있다. (총 2가지 API를 호출해야 한다.)
내용에 대한 경우, 수정 제출(PUT)과 새로운 입력 제출(POST)가 모두 있을 수 있다.
예를 들어서, useFieldArray로 추가된 항목은 POST로 제출되어야 하고, 기존에 있던 항목의 경우 변동 사항이 있을 때 PUT으로 제출되어야 한다.
입력 완료의 경우 첫 제출(POST)일 수도 있고, 이전 제출 사항을 수정한 경우(PATCH)일 수도 있다.
처음에는 단순히 순차적으로 API를 호출하는 방식으로 코드를 작성했다.
const onSubmit = () => {
if (getValues("postContent").length > 0) {
postContent(getValues("postContent"));
}
/* 수정 요청 시, 변경 안된 field는 반영하지 않음. */
if (getFieldState("putContent").isDirty) {
let tempIsDone: ContentUpdateT[] = getValues("putContent").map(
(v: ContentResponseT) => {
// 변경된 filed에 대한 처리
}
);
let tempChange = getValues("changed");
if (tempChange) {
let tempChangeRq: ContentUpdateT[] = tempChange.map((v: ContentResponseT) => {
// schema
});
putContent([...tempIsDone, ...tempChangeRq]);
} else {
putContent(tempIsDone);
}
}
/* 제출 관련 API 호출 */
if (isDoneBefore) {
//수정된 경우
toPatchIsDone();
} else {
toPostIsDone();
}
// 성공 시 로직
};
POST와 PUT에 대한 필터링이 필요해지며 코드가 너무 길어졌으며 성공과 실패 관련 부분을 붙이기 어려웠다. (중복 코드 생성)따라서 위의 조건을 만족시키기 위하여 Promise.all을 활용해보기로 했다.
fetch를 활용해 API 제출을 하는 방식임은 위와 동일하나, POST와 PUT에 대한 동시 호출 및 에러 핸들링을 가능해야 했기 때문에 Promise.all을 이용했다. 이 Promise.all에 대한 then-catch를 이용해 내용과 관련된 부분이 모두 완료되면 Form 입력 완료에 대한 API 호출을 하도록 설정했다.
Promise.all의 특징
이 메서드는 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다. 일반적으로 다음 코드를 계속 실행하기 전에 서로 연관된 비동기 작업 여러 개가 모두 이행되어야 하는 경우에 사용됩니다. 입력 값으로 들어온 프로미스 중 하나라도 거부 당하면
Promise.all()은 즉시 거부합니다.
const onSubmit = () => {
// flags
let toPostContent = undefined;
let toPutContent = undefined;
let toPostIsDone = undefined;
let toPatchIsDone = undefined;
/* 로직 처리 */
// API 요청 - 작업 목록
Promise.all([
toPostContent ? postContent(toPostContent) : null,
toPutContent ? putContent(toPutContent) : null,
])
.then(() => {
// 성공 시, isDone 수정
if (toPostIsDone) {
postIsDone(toPostIsDone)
.then(() => {
// 성공 시 로직
})
.catch((e) => {
console.log(e);
});
} else if (toPatchIsDone) {
patchIsDone(toPatchIsDone)
.then(() => {
// 성공 시 로직
})
.catch((e) => {
console.log(e);
});
} else {
// 업로드 실패
throw new Error("work log data is not defined");
}
})
.catch((e) => {
console.log(e);
});
};
useQuery를 이용하여 fetch를 수행함으로서 서버 데이터 컨트롤과 View 데이터 컨트롤을 분할하고 있었으나, useMutation을 사용하지 않아 그 경계가 모호해졌다.
코드 리뷰에서 mutateAsync 에 대해 언급해주셔서 관련 자료를 찾아보았다.
mutationAsync와 일반 mutate는 어떻게 다를까?
mutate는 아무 것도 반환하지 않는 반면,mutateAsync는 변형의 결과를 포함하는 프로미스를 반환합니다. 그래서 변형 응답에 접근해야 할 때mutateAsync를 사용하고 싶을 수 있지만, 저는 거의 항상mutate를 사용해야 한다고 주장하고 싶습니다.콜백을 통해
data나error에 여전히 접근할 수 있으며, 오류 처리에 대해 걱정할 필요가 없습니다.mutateAsync는 프로미스 제어권을 개발자에게 넘기기 때문에, 수동으로 오류를 잡아야 하며, 그렇지 않으면 처리되지 않은 프로미스 거부를 받을 수 있습니다.
mutateAsync가 더 우수한 상황은 정말로 프로미스가 필요한 경우입니다. 이는 여러 변형을 동시에 발동시키고 모두 완료되기까지 기다리고 싶거나, 콜백으로 인한 콜백 지옥에 빠질 수 있는 종속적인 변형이 있는 경우에 필요할 수 있습니다.
즉, mutateAsync를 사용한 이유는 Promise 형태로 이용하기 위해서였고 try-catch문을 통해 이를 구성할 수 있었다.
/*
mutations
- todoMutation : todo에 대한 POST OR PUT 수행, Promise.all로 병렬 동작
- logMutation : worklog에 대한 POST OR PATCH 수행
*/
const { mutateAsync: contentMutate } = useMutation({
mutationFn: async (data: {
toPostContent: ContentRequestT[] | undefined;
toPutContent: ContentUpdateT[] | undefined;
}) => {
await Promise.all([
data.toPostContent ? postContent(data.toPostContent) : null,
data.toPutContent ? putContent(data.toPutContent) : null,
]);
},
onError: () => {
// error 처리
},
});
const { mutateAsync: isDoneMutate } = useMutation({
mutationFn: async (data: {
toPostIsDone: RequestIsDoneT | undefined;
toPatchIsDone: PatchIsDoneT | undefined;
}) => {
if (data.toPostIsDone) {
await postIsDone(data.toPostIsDone);
} else if (data.toPatchIsDone) {
await patchIsDone(data.toPatchIsDone);
} else {
throw new Error("work log data is not defined");
}
},
onSuccess: () => {
// 성공 시 로직
},
onError: () => {
// error 처리
});
},
});
/* 최종 제출(mutation 포함) */
const onSubmit = async () => {
// flags
let toPostContent = undefined;
let toPutContent = undefined;
let toPostIsDone = undefined;
let toPatchIsDone = undefined;
/* 로직 처리 */
// mutation을 이용한 API 비동기 호출
// mutation 중 하나라도 오류 발생 시, catch
try {
await contentMutate({
toPostContent: toPostContent,
toPutContent: toPutContent,
});
await isDoneMutate({
toPostIsDone: toPostIsDone,
toPatchIsDone: toPatchIsDone,
});
} catch (error) {
console.log(error);
}
};
Promise와 동기 비동기는 항상 어려운 기분이다. 뭐든 끝은 없다지만 정말 얘는 가장 끝이 없는 기분이랄까.
그래서 그런지 아직 이 코드만으로 완전히 완성된 단계는 아니라고 생각된다. mutation에 대한 이해가 아직 부족해 제대로 코드 분리를 하지 못한 것도 있고, 보다 나은 제출 방법이 있을 거라 생각되어 이외의 방법도 찾아보고자 한다.
특히 mutateAsync를 제대로 이해하지 못해... 이 부분에 대해서 좀 더 테스트 해보고 답을 얻고자 한다!
혹시 좋은 아이디어가 있다면 댓글로 공유해주세요…🥺