|
| 1 | +--- |
| 2 | +title : "react-hook-form & forwardRef" |
| 3 | +author: potato |
| 4 | +date : 2025-02-08 12:18:00 +0900 |
| 5 | +categories : [React, TroubleShooting] |
| 6 | +tags: [react, javascript, typescript, troubleshooting,] |
| 7 | +image: |
| 8 | + path: https://github.com/user-attachments/assets/cc584f76-b207-47df-aae1-9605a42af368 |
| 9 | +description: react-hook-form 과 컴포넌트 분리에 따른 register의 forwardRef 에러 |
| 10 | +--- |
| 11 | + |
| 12 | +> 가계부 프로젝트 중 일어난 오류의 트러블슈팅입니다. |
| 13 | +
|
| 14 | +react-hook-form은 한 번 사용해보고 싶었기도 하고, 프로젝트를 같이 진행하는 팀원의 추천을 받아 사용하게 되었습니다. |
| 15 | + |
| 16 | +> React 애플리케이션에서 폼을 쉽게 관리하기 위한 라이브러리 중 하나입니다. 이 라이브러리는 더 나은 성능과 사용자 경험을 제공하며, React 컴포넌트의 상태 및 라이프사이클을 활용하여 폼 상태를 관리합니다. |
| 17 | +ref를 이용한 비제어 컴포넌트방식을 이용해 어떠한 값을 입력할 때에 리렌더링의 횟수를 줄여줍니다. 이는 제어형 컴포넌트에 비해 빠른 마운트 속도를 보여줍니다. |
| 18 | +{: .prompt-info } |
| 19 | + |
| 20 | +`watch`, `getValues` 등의 여러 함수를 통해 상태관리를 쉽게 관리할 수 있다는 점이 매력적이었습니다. |
| 21 | + |
| 22 | +수입과 지출을 입력할 수 있는 입력 페이지의 form을 만들다가 일어난 오류입니다. |
| 23 | + |
| 24 | + |
| 25 | +해당 페이지의 입력 form을 아래 구조와 같이 분리했습니다. |
| 26 | +``` |
| 27 | +src/components/Input |
| 28 | +├── CategorySelect.tsx |
| 29 | +├── InputContainer.tsx |
| 30 | +├── InputField.tsx |
| 31 | +├── InputForm.tsx |
| 32 | +└── InputTypeToggle.tsx |
| 33 | +``` |
| 34 | +**InputTypeToggle** |
| 35 | +: 수입 지출을 선택할 수 있는 컴포넌트 |
| 36 | + |
| 37 | +**InputField** |
| 38 | +: 사용 금액, 사용처, 카테고리, 사용한 날짜, 메모 Input을 만들기 위한 컴포넌트 |
| 39 | + |
| 40 | +**CategorySelect** |
| 41 | +: 카테고리 드롭다운을 구현한 컴포넌트 |
| 42 | + |
| 43 | +**InputForm** |
| 44 | +: InputTypeToggle, InputField, CategorySelect 컴포넌트의 값을 제출하기 위한 Form |
| 45 | + |
| 46 | +**InputContainer** |
| 47 | +: 최상위 부모 컨테이너 (흰색 박스) |
| 48 | + |
| 49 | + |
| 50 | +## 문제 |
| 51 | +### 문제 코드 |
| 52 | +```tsx |
| 53 | +... |
| 54 | +<form onSubmit={onSubmit}> |
| 55 | + <InputTypeToggle |
| 56 | + inputType={inputType} |
| 57 | + onTypeChange={handleTypeChange} |
| 58 | + /> |
| 59 | + |
| 60 | + <div className='flex w-full h-full gap-14 tablet:flex-row'> |
| 61 | + <div className='flex flex-col flex-1'> |
| 62 | + <InputField label='사용 금액' unit='원' type='number' {...register('amount')} /> |
| 63 | + <InputField label='사용처' {...register('place')} /> |
| 64 | + <CategorySelect |
| 65 | + selected={categories[0]} |
| 66 | + onChange={handleCategoryChange} |
| 67 | + options={categories} |
| 68 | + /> |
| 69 | + </div> |
| 70 | + <div className='flex flex-col flex-1'> |
| 71 | + <InputField label='사용한 날짜' type='date' {...register('date')} /> |
| 72 | + <InputField label='메모' tagName='textarea' {...register('memo')} /> |
| 73 | + </div> |
| 74 | + </div> |
| 75 | +</form> |
| 76 | +... |
| 77 | +``` |
| 78 | +저는 중복되는 코드를 줄여 가독성과 재사용성을 높이기 위해 컴포넌트를 분리했습니다. 그리고 react-hook-form의 register 기능을 사용하려고 했습니다. |
| 79 | + |
| 80 | +코드는 문제 없이 작동하는 듯 했습니다. 하지만... |
| 81 | + |
| 82 | + |
| 83 | +_아 안사요_ |
| 84 | + |
| 85 | +콘솔을 확인하니 함수 컴포넌트는 refs 를 받을 수 없다는 Warning이 발생했습니다. |
| 86 | + |
| 87 | +react-hook-form 을 처음 사용해 이해가 부족한 상황이라, 바로 구글링을 시작했습니다. |
| 88 | + |
| 89 | +## 해결 |
| 90 | +[react-hook-form & Input with forwardRef](https://velog.io/@xowns3213/react-hook-form-with-forwardRef) |
| 91 | + |
| 92 | +[React 공식 사이트](https://react.dev/reference/react/forwardRef) |
| 93 | + |
| 94 | +해당 블로그와 공식 사이트가 큰 도움이 되었습니다. |
| 95 | + |
| 96 | +register에는 onChange, required 등과 함께 ref도 포함하고 있습니다. |
| 97 | +결국 저렇게 작성을 하면 ref를 props로 넘기는 것이고 위의 에러가 발생되는 것입니다. |
| 98 | + |
| 99 | +> React 19 버전에서는 더 이상 `forwardRef` 가 필요하지 않다라고 명시되어 있지만, 저는 18 버전을 사용하기 때문에 `forwardRef` 를 통해 ref 를 전달해주어야 합니다. |
| 100 | +
|
| 101 | +저는 `InputForm` 컴포넌트에서 register 함수를 자식에게 넘겨주어 사용하게 하려고 합니다. |
| 102 | + |
| 103 | +즉 `register` 함수를 `<input>` DOM 노드에 직접 사용해야하기 때문에, 부모 컴포넌트에 DOM 노드를 노출시켜야 합니다. |
| 104 | + |
| 105 | +따라서 `register` 함수를 받아 사용해야 하는 컴포넌트에 `forwardRef` 를 추가해줍니다. |
| 106 | + |
| 107 | +### 수정된 코드 |
| 108 | +```tsx |
| 109 | +... |
| 110 | +interface InputFieldProps |
| 111 | + extends React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> { |
| 112 | + label: string |
| 113 | + unit?: string |
| 114 | + type?: string |
| 115 | + tagName?: 'input' | 'textarea' |
| 116 | +} |
| 117 | + |
| 118 | +const InputField = React.forwardRef< |
| 119 | + HTMLInputElement | HTMLTextAreaElement, |
| 120 | + InputFieldProps |
| 121 | +>(({ label, unit, type = 'text', tagName }, ref) => { |
| 122 | + return ( |
| 123 | + <div className={`mb-11 ${tagName === 'textarea' && 'flex-1'}`}> |
| 124 | + <div className='flex justify-between items-center mb-2'> |
| 125 | + <label className='text-2xl font-medium ml-0.5'>{label}</label> |
| 126 | + {unit && <label>({unit})</label>} |
| 127 | + </div> |
| 128 | + <div |
| 129 | + className={`relative flex rounded-[15px] w-full bg-[#F7F7F8] shadow-analyze-box tablet:flex-row flex-1 ${tagName === 'textarea' ? 'h-full' : 'h-16'}`} |
| 130 | + > |
| 131 | + {tagName === 'textarea' ? ( |
| 132 | + <textarea |
| 133 | + className='text-xl px-5 py-5 w-full rounded-[15px] focus:ring-2 focus:ring-inset focus:ring-[#5FB1FF] focus:outline-none bg-transparent' |
| 134 | + ref={ref as React.Ref<HTMLTextAreaElement>} |
| 135 | + /> |
| 136 | + ) : ( |
| 137 | + <input |
| 138 | + type={type} |
| 139 | + className='text-xl px-5 w-full rounded-[15px] focus:ring-2 focus:ring-inset focus:ring-[#5FB1FF] focus:outline-none bg-transparent' |
| 140 | + ref={ref as React.Ref<HTMLInputElement>} |
| 141 | + /> |
| 142 | + )} |
| 143 | + </div> |
| 144 | + </div> |
| 145 | + ) |
| 146 | +}) |
| 147 | +... |
| 148 | +``` |
| 149 | + |
| 150 | +React 18에서 ref 를 전달할 때는 `forwardRef` 를 꼭 사용하도록 하겠읍니다... |
0 commit comments