Radix-UI Select + React Hook Form 사용시 ref 전달 처리 에러 대응

Radix UI의 Select 컴포넌트와 React-Hook-Form을 이용해서 처리할때 ref 전달시 문제가 생기는 경우가 있습니다. React-hook-form의 Controller와 forwardRef를 활용한 방법으로 이 문제를 해결할 수 있습니다.

Radix-UI Select + React Hook Form  사용시 ref 전달 처리 에러 대응
Photo by Lautaro Andreani / Unsplash

Radix-UIReact Hook Form 을 처음 써보면서 삽질한 기록입니다.

기본적으로 Radix-UI는 headless UI Component 컨셉으로 만들어진 라이브러리이지만, @radix-ui/themes 를 이용하면 기본적인 스타일은 적용되어있기 때문에 편리합니다.

React Hook Form 도 처음 써보는데, Radix UI 와 함께 쓰다보니 소소한 문제가 좀 있습니다.

HTML에서 보여지는 form 요소들은 대체로 좀 못생겼습니다. 스타일이 적용되지 않는 구성요소들도 있는데, 그중에 대표적인게 select 태그입니다.

그래서 스타일을 입히려고 하면 우회해서 처리하는 방법들을 써야합니다.

그러다보니 Radix UISelect 컴포넌트도 대략 흝어보니 ref를 이용해서 하위 컴포넌트로 값을 전달하는 방식인 것 같습니다.


이런 방식으로 쓰이는 컴포넌트에 대응하기 위해서 React hook form 에서도 ref 를 전달해줄 수 있습니다. 그런데 Select.Root 컴포넌트에 ref를 그대로 전달해주면 문제가 좀 생깁니다.

💡
함수형 컴포넌트에는 바로 ref를 전달할 수 없다는 에러메세지가 출력됩니다.

그래서 이 부분을 해결해보고자 Chat GPT에 물어봤더니 아래와 같은 답을 내주더군요.

Radix UI의 Select 컴포넌트와 React Hook Form간의 ref 에러 해결 방안

Radix UISelect 컴포넌트를 사용할 때 react-hook-form에서 ref 에러가 발생하는 이유는 주로 Radix UIreact-hook-form 간의 참조(ref) 호환성 문제 때문입니다. react-hook-form은 주로 네이티브 HTML 폼 요소들과 직접적으로 연동되도록 설계되어 있으며, 이러한 요소들은 ref 속성을 사용하여 DOM 요소에 직접 접근합니다.

그러나 Radix UI와 같은 커스텀 컴포넌트 라이브러리들은 네이티브 HTML 요소를 직접 노출시키지 않을 수 있으며, 이는 react-hook-formregister 함수와 해당 컴포넌트의 ref를 연결하는 데 문제를 일으킬 수 있습니다. 결과적으로, react-hook-form이 해당 컴포넌트의 인스턴스에 접근할 수 없어 ref 에러가 발생합니다.

이 문제를 해결하기 위한 몇 가지 방법은 다음과 같습니다:

  1. Controller 사용: react-hook-formController 컴포넌트를 사용하여 Radix UISelect 컴포넌트를 감싸고, ControllerSelect 컴포넌트와의 상호작용을 관리하도록 할 수 있습니다. 이 방법은 react-hook-form이 제공하는 컨트롤러를 통해 비표준 폼 요소와의 통합을 용이하게 합니다.
  2. forwardRef 사용: Radix UI 컴포넌트를 감싸는 래퍼 컴포넌트를 만들고, React.forwardRef를 사용하여 내부적으로 ref를 전달할 수 있습니다. 이렇게 하면 react-hook-form이 필요한 ref에 접근할 수 있게 됩니다.
  3. onValueChange 사용: Radix UISelect 컴포넌트에서 onValueChange 이벤트를 사용하여 선택된 값을 react-hook-formsetValue 메소드를 통해 수동으로 설정할 수 있습니다. 이 방법은 ref를 사용하지 않고도 폼의 상태를 관리할 수 있게 해줍니다.

여러가지 방법을 소개해줬는데, 단순하게 해결되지는 않더군요.

1,2,3번을 모두 각각 시도해도 잘 되지 않아서, ControllerforwardRef를 이용하는 1,2번을 조합해서 시도했음에도 잘 안됐습니다. 그러다가 아래 글에서 힌트를 좀 얻었어요

How can I implement React Hook Form with Radix UI Select?
I’m using the radix ui primitive Select, but i don’t know how to integrate it with React Hook Form. I’ve tried putting the register in the Select.Root tag but it didn’t work.I’m also using Styled

그 결과가 아래 소스코드입니다.

// CustomSelect.jsx
import React from "react"
import { Select } from "@radix-ui/themes"

const CustomSelect = React.forwardRef((props, ref) => (
  <Select.Root {...props}>
    <Select.Trigger />
    {/* ref를 Select.Root가 아닌 Select.Content에 전달 */}
    <Select.Content position="popper" ref={ref}>
      <Select.Item value="option1">Option 1</Select.Item>
      <Select.Item value="option2">Option 2</Select.Item>
    </Select.Content>
  </Select.Root>
))

export default CustomSelect
// Form.jsx
import {Controller, useForm} from "react-hook-form"
import CustomSelect from "@/app/ui/CustomSelect"

const Form = () => {
    const {
        register,
        handleSubmit,
        watch,
        control,
        formState: {errors},
    } = useForm({
        defaultValues: {
            option: "option1",
        },
    })
    const submitHandler = async (data) => {
        console.log(data)
    }
    return (
        <form onSubmit={handleSubmit(submitHandler)}>
            <Controller
                name="option"
                control={control}
                render={({field}) => (
                    {/* onValueChange를 CustomSelect 컴포넌트 외부에서 연결*/}
                    <CustomSelect onValueChange={field.onChange} {...field} />
                )}
            />
            <Button variant={"surface"} radius={"small"} type={"submit"} size={"3"}>
                제출
            </Button>
        </form>
    )
}
export default Form

보통 해결 방안으로 알려주는 것들은 모든 propsSelectComponent 안에서 받아서 처리하도록 나오는데, onValueChange 를 컴포넌트 외부에서 적용해주고, refSelect.Root 컴포넌트가 아닌 Select.Content 에 전달해주었습니다.

오류메세지는 출력되지 않고 제대로 값이 전달되고 있기 때문이 이렇게 일단 적용하는데, 정확한 부분은 차차 더 뜯어봐야 할 것 같습니다.