[resolve] React Hook Form reset을 사용했을 때 shadcn Select가 초기화되지 않는 문제

React, React Query, shadcn/ui, React Hook Form을 사용해서 게시물 수정 폼을 구현하던 중 겪은 문제다.
일반적인 게시물 수정 페이지였고 서버에서 기존 게시물 데이터를 불러온 뒤 react-hook-form의 reset을 사용해서 폼 값을 채워주는 구조였다.
텍스트 입력 값이나 다른 필드들은 정상적으로 반영됐다.
그런데 이상하게도 Select 컴포넌트만 기존 값이 화면에 반영되지 않았다.
문제 상황
기존 코드는 다음과 같았다.
<Controller
control={control}
name="department_code"
render={({ field }) => (
<Select
name="department_code"
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="부서 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="-1">부서 선택</SelectItem>
{teams.map((team) => (
<SelectItem
key={team.department_code}
value={team.department_code}
>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
서버에서 데이터를 가져온 뒤에는 대략 이런 식으로 값을 넣고 있었다.
useEffect(() => {
if (!data) return;
reset({
title: data.title,
content: data.content,
department_code: data.department_code,
});
}, [data, reset]);
title, content 같은 값은 정상적으로 폼에 반영됐다.
하지만 department_code만 reset 이후에도 Select UI에 표시되지 않았다.
React Hook Form DevTools나 watch로 값을 확인해보면 실제 form state에는 값이 들어가 있었다.
즉, 문제는 React Hook Form이 값을 못 받는 것이 아니라 shadcn Select UI가 변경된 값을 제대로 반영하지 못하는 쪽에 가까웠다.
shadcn 공식 예제와 비교
처음에는 내가 구현을 잘못한 줄 알았다.
그래서 shadcn 공식 문서에 있는 React Hook Form + Select 예제를 다시 확인했다.
<Controller
name="language"
control={form.control}
render={({ field, fieldState }) => (
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
{spokenLanguages.map((language) => (
<SelectItem key={language.value} value={language.value}>
{language.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
공식 예제에서도 핵심은 동일했다.
<Select
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
내 코드도 이 구조를 따르고 있었다.
value도 넣어줬고, onValueChange도 field.onChange와 연결했다.
겉으로 보면 controlled component 처럼 정상적으로 동작해야 했다.
하지만 실제로는 reset 이후 Select의 표시 값이 갱신되지 않았다.
원인
검색을 해보니 같은 증상을 겪은 이슈가 있었다.
shadcn/ui GitHub Issue 중 React Hook Form reset method does not reset the value selected by the user in the Select 라는 내용이었다.
이 이슈에서 제안된 해결 방법 중 하나는 Select 컴포넌트에 key를 설정하는 것이었다.
shadcn의 Select는 내부적으로 Radix UI Select를 기반으로 동작한다.
이런 컴포넌트는 단순한 HTML select와 다르게 내부 상태, trigger, value 표시 영역, popover content 등이 조합된 컴포넌트다.
React Hook Form의 reset으로 form state는 변경되지만 Select 내부에서 이미 만들어진 상태나 표시 영역이 기대한 시점에 다시 동기화되지 않을 수 있다.
이때 key를 변경해주면 React는 해당 컴포넌트를 기존 컴포넌트의 업데이트로 보지 않고 새 컴포넌트로 다시 마운트한다.
즉, reset으로 변경된 값을 기준으로 Select를 다시 렌더링하게 만들 수 있다.
해결 방법
수정한 코드는 다음과 같다.
<Controller
control={control}
name="department_code"
render={({ field }) => (
<Select
key={field.value}
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder="부서 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="-1">부서 선택</SelectItem>
{teams.map((team) => (
<SelectItem
key={team.department_code}
value={team.department_code}
>
{team.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
핵심은 이 부분이다.
key={field.value}
field.value가 바뀔 때마다 Select 컴포넌트를 다시 마운트하도록 한 것이다.
이렇게 수정하자 reset 이후에도 Select 값이 정상적으로 화면에 반영됐다.
정리
이번 문제는 React Hook Form의 reset이 동작하지 않은 문제가 아니었다.
실제로 form state에는 값이 정상적으로 들어가고 있었다.
다만 shadcn Select 컴포넌트의 표시 상태가 reset 이후 값 변경을 제대로 반영하지 못한 문제였다.
최종적으로는 Select 컴포넌트에 key={field.value}를 추가해서 해결했다.
<Select
key={field.value}
name={field.name}
value={field.value}
onValueChange={field.onChange}
>
Reference
https://github.com/react-hook-form/react-hook-form/blob/master/src/useForm.ts
react-hook-form/src/useForm.ts at master · react-hook-form/react-hook-form
📋 React Hooks for form state management and validation (Web + React Native) - react-hook-form/react-hook-form
github.com
https://ui.shadcn.com/docs/forms/react-hook-form
React Hook Form
Build forms in React using React Hook Form and Zod.
ui.shadcn.com
https://github.com/shadcn-ui/ui/issues/549
Unable to reset the Select component with React Hook Form · Issue #549 · shadcn-ui/ui
Calling form.reset() (React Hook Form reset method) does not reset the value selected by the user in the Select. Here's my useForm hook usage: const form = useForm<z.infer<typeof someSchema>>({ res...
github.com