React Hook Form + TanStack Query 데이터 흐름 정리

Melon Coder·2026년 1월 20일

Trouble shooting

목록 보기
6/9

배경

직원 정보 수정 모달을 만들면서 데이터 흐름이 좀 헷갈렸다.
API에서 데이터 가져오고, 폼에 채우고, 수정하고, 다시 저장하는 과정이 생각보다 복잡했다.


전체 흐름

API 조회 → useForm에 초기값 세팅 → 사용자가 수정 → mutate 호출 → 성공 → form.reset + 캐시 업데이트

하나씩 뜯어보자.


1단계: API 조회

const { data: officeData } = useGetUserOffice(userId.toString())

TanStack Query로 서버에서 데이터를 가져온다. officeData에 기존 직원 정보가 담긴다.


2단계: useForm에 초기값 세팅

const form = useForm<UserOfficeDtoType>({
  defaultValues: {
    ...defaultData,
    department: mapLabelToValue(officeDepartmentNameOption, defaultData.department),
    position: mapLabelToValue(officePositionOption, defaultData.position),
    // ...
  }
})

여기서 좀 헤멤;;
서버에서 오는 값이 "경영지원"(label)인데, 폼에서는 "MANAGEMENT_SUPPORT"(value)로 저장해야 했다.
mapLabelToValue 함수로 변환해서 넣어줬다.


3단계: 사용자가 수정

<MultiInputBox name='department' form={form} labelMap={MEMBER_INPUT_INFO.office} />

MultiInputBox 내부에서 Controller가 폼 상태를 관리한다.

<Controller
  name={name}
  control={form.control}
  render={({ field }) => (
    <TextField
      value={field.value}
      onChange={field.onChange}  // 여기서 form 상태 업데이트
      select
    />
  )}
/>

드롭다운에서 값을 바꾸면 field.onChange가 호출되고, form 내부 상태가 바뀐다. 이 시점에서 서버에는 아무것도 안 간다.


4단계: 저장 버튼 클릭 → mutate 호출

const { mutateAsync } = useMutateSingleMember<UserOfficeDtoType>(userId, 'office')

const save = form.handleSubmit(async data => {
  const newOffice = await mutateAsync(data)
  // ...
})

form.handleSubmit이 폼 데이터를 모아서 data로 넘겨준다. 이걸 mutateAsync로 서버에 보낸다.
처음에 mutatemutateAsync 차이를 몰랐다. mutateAsync는 Promise를 반환해서 await로 기다릴 수 있다. 저장 성공 후에 뭔가 처리해야 하면 mutateAsync를 쓰면 된다.


5단계: 성공 후 처리

const save = form.handleSubmit(async data => {
  const newOffice = await mutateAsync(data)

  // 폼 상태 리셋 (dirty 플래그 초기화)
  form.reset({
    ...newOffice,
    department: newOffice.department ?? '',
    // ...
  })

  console.log('office 정보 수정 완료')
})

여기서 form.reset을 안 하면 문제가 생긴다. 저장 후에도 폼이 "수정됨" 상태로 남아있어서, 창을 닫을 때 "변경사항이 있습니다" 경고가 뜬다.
form.reset(newData)를 호출하면 폼의 defaultValues가 새 데이터로 바뀌고, isDirtyfalse가 된다.


useMutation 내부에서는 캐시 업데이트

return useMutation({
  mutationFn: data => putSingleMember(memberId, data),
  onSuccess: data => {
    queryClient.setQueryData(queryKey, (prev) => ({
      ...prev,
      [requestInfo.dtoKey]: { ...prev[requestInfo.dtoKey], ...data }
    }))
  }
})

onSuccess에서 queryClient.setQueryData로 캐시를 업데이트한다. 이러면 다른 곳에서 같은 데이터를 조회할 때 서버 요청 없이 바로 새 데이터를 보여줄 수 있다.


헷갈렸던 부분

1. defaultValues vs reset

처음에 API 응답이 오면 defaultValues가 자동으로 바뀌는 줄 알았다. 아니었다. defaultValues는 최초 1회만 적용된다. 나중에 값을 바꾸려면 form.reset(newValues)를 써야 한다.

2. isDirty 체크

저장 버튼을 "변경사항 있을 때만" 활성화하고 싶었다.

<Button disabled={!form.formState.isDirty}>저장</Button>

근데 탭이 여러 개라서 각 탭의 isDirty를 모아서 체크해야 했다. useImperativeHandle로 각 탭 컴포넌트의 dirty 상태를 부모에서 접근할 수 있게 했다.


정리

대충 데이터가 서버 → 폼 → 서버 → 캐시 → 폼으로 돌아오는 흐름을 이해하니까 어디서 뭘 해야 하는지 감이 잡혔다.

profile
Frontend developer

0개의 댓글