입력 폼에서 문구를 추가하는 기능 구현 중, 전송된 값이 빈 문자열로 도착하는 이슈를 겪었다. 상태와 폼 이벤트는 정상처럼 보였으나, 뮤테이션 훅 설계에서 최신 상태가 반영되지 않는 구조적 문제가 원인이었다. 본 글은 왜 이런 현상이 생겼는지, 그리고 이를 해결한 과정을 정리한다.
사용자가 입력창에 문구를 적고 제출해도 서버로 전송되는 데이터의 saying 값이 항상 빈 문자열이었다. 콘솔과 상태는 정상적으로 변했지만 네트워크 탭에서 확인한 요청 바디에는 최신 값이 반영되지 않았다. 입력 검증도 통과했기 때문에 폼 구성 자체의 오류보다는 데이터 전달 경로의 문제로 보였다.
뮤테이션 훅을 만들 때 page, buyLevel과 함께 saying을 인자로 넘기고, 내부 mutationFn에서 그 saying을 그대로 참조했다. 이 패턴은 훅 생성 시점의 saying을 클로저로 캡처하여 이후 제출 시점의 최신 입력값이 아닌 과거의 값을 사용하게 만든다. 초기값이 빈 문자열이었다면 매 호출마다 빈 문자열이 서버로 전송되는 현상이 발생한다.
뮤테이션 훅에서 saying을 제거하고, mutate 호출 시점에 인자로 전달하도록 구조를 변경했다. 즉, 훅은 비교적 변동이 적은 page와 buyLevel만 받고, 자주 변하는 saying은 mutationFn의 파라미터로 받아 API에 전달한다. 컴포넌트에서는 addFortuneSayingByBuyLevel({ saying })으로 최신 입력값을 넘기도록 수정하는 것으로 해결할 수 있었다.
export function useAddFortuneSayingByBuyLevel({
page,
buyLevel,
saying,
}: {
page: number;
buyLevel: number;
saying: string;
}) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => addFortuneSayingByBuyLevel({ buyLevel, saying }), // 생성 시점 saying 캡처
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: FORTUNE_SAYING_QUERY_KEYS.fortuneSayingsByBuyLevel({ page, buyLevel }),
});
},
});
}
export function useAddFortuneSayingByBuyLevel({ page, buyLevel }: { page: number; buyLevel: number }) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ saying }: { saying: string }) => addFortuneSayingByBuyLevel({ buyLevel, saying }),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: FORTUNE_SAYING_QUERY_KEYS.fortuneSayingsByBuyLevel({ page, buyLevel }),
});
},
});
}
리액트에서 비동기 뮤테이션을 설계할 때, 자주 변하는 입력값을 훅 생성 시점의 옵션으로 넘겨 클로저에 고정시키면 최신성이 깨져 의도치 않은 데이터가 전송될 수 있다. 변하는 값은 mutate 호출 시점에 변수로 전달하는 패턴을 사용해야한다는 것을 깨달았다. 이 접근은 상태 최신성을 보장하며, “입력은 보이는데 서버엔 빈 값이 간다” 같은 문제를 근본적으로 예방한다.