Skip to content

Commit a9bc718

Browse files
committed
[Posts] react-hook-form 트러블슈팅
1 parent e7a34bf commit a9bc718

8 files changed

Lines changed: 164 additions & 14 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
![Image](https://github.com/user-attachments/assets/65476cca-b34b-4c10-ac8d-e60628a450cc)
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+
![Image](https://github.com/user-attachments/assets/c1e75b58-9be9-4183-82ba-2471868cda85)
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` 를 꼭 사용하도록 하겠읍니다...

_site/about/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
<meta property="og:url" content="http://localhost:4000/about/" />
3939
<meta property="og:site_name" content="코딩하는 감자" />
4040
<meta property="og:type" content="article" />
41-
<meta property="article:published_time" content="2025-02-05T15:09:08+09:00" />
41+
<meta property="article:published_time" content="2025-02-08T03:03:00+09:00" />
4242
<meta name="twitter:card" content="summary" />
4343
<meta property="twitter:title" content="About" />
4444
<script type="application/ld+json">
45-
{"@context":"https://schema.org","@type":"WebSite","dateModified":"2025-02-05T15:09:08+09:00","datePublished":"2025-02-05T15:09:08+09:00","description":"Add Markdown syntax content to file _tabs/about.md and it will show up on this page.","headline":"About","name":"이어진","sameAs":["https://github.com/oding01"],"url":"http://localhost:4000/about/"}</script>
45+
{"@context":"https://schema.org","@type":"WebSite","dateModified":"2025-02-08T03:03:00+09:00","datePublished":"2025-02-08T03:03:00+09:00","description":"Add Markdown syntax content to file _tabs/about.md and it will show up on this page.","headline":"About","name":"이어진","sameAs":["https://github.com/oding01"],"url":"http://localhost:4000/about/"}</script>
4646
<!-- End Jekyll SEO tag -->
4747

4848

_site/archives/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
<meta property="og:url" content="http://localhost:4000/archives/" />
3939
<meta property="og:site_name" content="코딩하는 감자" />
4040
<meta property="og:type" content="article" />
41-
<meta property="article:published_time" content="2025-02-05T15:09:08+09:00" />
41+
<meta property="article:published_time" content="2025-02-08T03:03:00+09:00" />
4242
<meta name="twitter:card" content="summary" />
4343
<meta property="twitter:title" content="Archives" />
4444
<script type="application/ld+json">
45-
{"@context":"https://schema.org","@type":"BlogPosting","dateModified":"2025-02-05T15:09:08+09:00","datePublished":"2025-02-05T15:09:08+09:00","description":"우당탕탕 와르르멘션 개발일지","headline":"Archives","mainEntityOfPage":{"@type":"WebPage","@id":"http://localhost:4000/archives/"},"url":"http://localhost:4000/archives/"}</script>
45+
{"@context":"https://schema.org","@type":"BlogPosting","dateModified":"2025-02-08T03:03:00+09:00","datePublished":"2025-02-08T03:03:00+09:00","description":"우당탕탕 와르르멘션 개발일지","headline":"Archives","mainEntityOfPage":{"@type":"WebPage","@id":"http://localhost:4000/archives/"},"url":"http://localhost:4000/archives/"}</script>
4646
<!-- End Jekyll SEO tag -->
4747

4848

_site/assets/js/data/search.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414

1515

16-
"snippet": "모바일 청첩장 프로젝트에서, 열심히 캘린더를 만들어 레포지토리에 푸쉬했습니다. 그런데…문제이렇게 부모 컨테이너를 벗어나 overflow-scroll이 되어버리는 현상이 발생했습니다.전체 코드import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/comp...",
17-
"content": "모바일 청첩장 프로젝트에서, 열심히 캘린더를 만들어 레포지토리에 푸쉬했습니다. 그런데…문제이렇게 부모 컨테이너를 벗어나 overflow-scroll이 되어버리는 현상이 발생했습니다.전체 코드import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';const days = ['일', '월', '화', '수', '목', '금', '토'];const CalendarContent = () =&gt; { const firstDay = new Date(2025, 3, 1).getDay(); // 화요일 = 2 const lastDate = new Date(2025, 4, 0).getDate(); // 30일 const dates = Array(firstDay) .fill(null) .concat([...Array(lastDate)].map((_, i) =&gt; i + 1)); return ( &lt;div className=\"w-full h-full leading-9\"&gt; &lt;div className=\"font-medium text-2xl tracking-wide\"&gt;2025.04.05&lt;/div&gt; &lt;div className=\"mb-5 tracking-wider\"&gt;토요일 오후 3시&lt;/div&gt; &lt;div className=\"text-center px-10 pb-10\"&gt; &lt;Table className=\"\"&gt; &lt;TableHeader&gt; &lt;TableRow&gt; {days.map((day, index) =&gt; ( &lt;TableHead key={index} className={ day === '일' ? 'text-[#c6472b] text-center' : 'text-center' } &gt; {day} &lt;/TableHead&gt; ))} &lt;/TableRow&gt; &lt;/TableHeader&gt; &lt;TableBody&gt; {Array.from({ length: Math.ceil(dates.length / 7) }, (_, week) =&gt; ( &lt;TableRow key={week}&gt; {dates.slice(week * 7, (week + 1) * 7).map((date, i) =&gt; ( &lt;TableCell key={i} className={ (i === 0 ? 'text-[#c6472b]' : '') || (date === 5 ? 'bg-[#858585] text-white rounded-full' : '') } &gt; {date || ''} &lt;/TableCell&gt; ))} &lt;/TableRow&gt; ))} &lt;/TableBody&gt; &lt;/Table&gt; &lt;/div&gt; &lt;/div&gt; );};export default CalendarContent;문제가 되는 부분은 이 부분인 것 같습니다....&lt;div className=\"text-center px-10 pb-10\"&gt; &lt;Table className=\"\"&gt;...현재 shadCn의 UI 컴포넌트를 사용하고 있는데, Table 컴포넌트의 바깥쪽 컨테이너와 안 쪽 컨테이너를 보시면 w-full이 적용되어 있고, 바깥쪽에는 overflow-auto가 적용되어 있는 것을 볼 수 있습니다.const Table = React.forwardRef&lt; HTMLTableElement, React.HTMLAttributes&lt;HTMLTableElement&gt;&gt;(({ className, ...props }, ref) =&gt; ( &lt;div className=\"w-full overflow-auto\"&gt; &lt;table ref={ref} className={cn(\"w-full caption-bottom text-sm\", className)} {...props} /&gt; &lt;/div&gt;))Table.displayName = \"Table\"생각이전 코드를 보니 px-10으로 왼쪽과 오른쪽에 패딩을 주고 있었는데, 이 패딩값과 테이블의 전체 너비가 맞지 않아서 발생하는 문제일 수 있겠다라는 생각을 했습니다.그래서 px-10을 제거하고, 테이블을 감싸는 컨테이너에 max-w-full을 적용해보았습니다. max-w-full max-width: 100% 를 적용해주는 Tailwind 클래스입니다. 이 속성은 요소가 부모 컨테이너의 너비보다 커지는 것을 방지해줍니다.즉, 요소가 부모보다 커지려고 하면 부모 width 만큼만 커지도록 제한하고, 요소가 부모보다 작다면 요소의 원래 크기를 유지합니다. "
16+
"snippet": "모바일 청첩장 프로젝트에서, 열심히 캘린더를 만들어 레포지토리에 푸쉬했습니다. 그런데…문제 - 1이렇게 부모 컨테이너를 벗어나 overflow-scroll이 되어버리는 현상이 발생했습니다.전체 코드import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/...",
17+
"content": "모바일 청첩장 프로젝트에서, 열심히 캘린더를 만들어 레포지토리에 푸쉬했습니다. 그런데…문제 - 1이렇게 부모 컨테이너를 벗어나 overflow-scroll이 되어버리는 현상이 발생했습니다.전체 코드import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';const days = ['일', '월', '화', '수', '목', '금', '토'];const CalendarContent = () =&gt; { const firstDay = new Date(2025, 3, 1).getDay(); // 화요일 = 2 const lastDate = new Date(2025, 4, 0).getDate(); // 30일 const dates = Array(firstDay) .fill(null) .concat([...Array(lastDate)].map((_, i) =&gt; i + 1)); return ( &lt;div className=\"w-full h-full leading-9\"&gt; &lt;div className=\"font-medium text-2xl tracking-wide\"&gt;2025.04.05&lt;/div&gt; &lt;div className=\"mb-5 tracking-wider\"&gt;토요일 오후 3시&lt;/div&gt; &lt;div className=\"text-center px-10 pb-10\"&gt; &lt;Table className=\"\"&gt; &lt;TableHeader&gt; &lt;TableRow&gt; {days.map((day, index) =&gt; ( &lt;TableHead key={index} className={ day === '일' ? 'text-[#c6472b] text-center' : 'text-center' } &gt; {day} &lt;/TableHead&gt; ))} &lt;/TableRow&gt; &lt;/TableHeader&gt; &lt;TableBody&gt; {Array.from({ length: Math.ceil(dates.length / 7) }, (_, week) =&gt; ( &lt;TableRow key={week}&gt; {dates.slice(week * 7, (week + 1) * 7).map((date, i) =&gt; ( &lt;TableCell key={i} className={ (i === 0 ? 'text-[#c6472b]' : '') || (date === 5 ? 'bg-[#858585] text-white rounded-full' : '') } &gt; {date || ''} &lt;/TableCell&gt; ))} &lt;/TableRow&gt; ))} &lt;/TableBody&gt; &lt;/Table&gt; &lt;/div&gt; &lt;/div&gt; );};export default CalendarContent;문제가 되는 부분은 이 부분인 것 같습니다....&lt;div className=\"text-center px-10 pb-10\"&gt; &lt;Table className=\"\"&gt;...현재 shadCn의 UI 컴포넌트를 사용하고 있는데, Table 컴포넌트의 바깥쪽 컨테이너와 안 쪽 컨테이너를 보시면 w-full이 적용되어 있고, 바깥쪽에는 overflow-auto가 적용되어 있는 것을 볼 수 있습니다.const Table = React.forwardRef&lt; HTMLTableElement, React.HTMLAttributes&lt;HTMLTableElement&gt;&gt;(({ className, ...props }, ref) =&gt; ( &lt;div className=\"w-full overflow-auto\"&gt; &lt;table ref={ref} className={cn(\"w-full caption-bottom text-sm\", className)} {...props} /&gt; &lt;/div&gt;))Table.displayName = \"Table\"트러블 슈팅 - 1부모 컨테이너의 너비를 벗어나고 있는 애는 table 태그 요녀석이었습니다.이전 코드를 보니 px-10으로 왼쪽과 오른쪽에 패딩을 주고 있었는데, 이 패딩값과 테이블의 전체 너비가 맞지 않아서 발생하는 문제일 수 있겠다라는 생각을 했습니다.그래서 px-10을 px-4로 줄여보고, 부모 너비를 넘지 않는 선에서 자신의 컨텐츠 크기만큼 차지하게 하기 위해 테이블 태그에 max-w-full을 적용해보았습니다. max-w-full max-width: 100% 를 적용해주는 Tailwind 클래스입니다. 이 속성은 요소가 부모 컨테이너의 너비보다 커지는 것을 방지해줍니다.즉, 요소가 부모보다 커지려고 하면 부모 width 만큼만 커지도록 제한하고, 요소가 부모보다 작다면 요소의 원래 크기를 유지합니다. 결과 - 1여전히 overflow되어 scroll이 생겨있었습니다… 그리고 원래 요소의 크기를 유지하다보니 화면의 너비가 커지면 왼쪽에 치우쳤습니다.트러블 슈팅 - 2치우쳤다면.. 가운데에 놓으면 되지 않나!table 태그를 감싸고 있던 div 컨테이너에 flex justify-center 를 추가하고, 스크롤이 생기는 것을 방지하기 위해 overflow-auto를 지워줍니다.table 태그의 max-w-full은 다시 부모 컨테이너의 100%를 유지하기 위해 w-full로 돌려놔주고, justify-center로 인해 남는 공간을 모두 차지하기 위해 flex-1을 추가합니다.최종 결과가로 길이가 제일 작았던 Galaxy Z Fold 5 (344px) 에서도 스크롤과 잘림 없이 제대로 가운데에 위치해있는 걸 볼 수 있었습니다! 성공!➕ 아예 자식 요소들의 width를 정해줄 수도 있지만 유연하게 가고 싶어서 flex를 선택해봤습니다."
1818
},
1919

2020
{

_site/assets/js/data/swconf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const swconf = {
22

3-
cacheName: 'chirpy-1738735748',resources: [
3+
cacheName: 'chirpy-1738951381',resources: [
44
'/assets/css/jekyll-theme-chirpy.css',
55
'/',
66

_site/categories/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
<meta property="og:url" content="http://localhost:4000/categories/" />
3939
<meta property="og:site_name" content="코딩하는 감자" />
4040
<meta property="og:type" content="article" />
41-
<meta property="article:published_time" content="2025-02-05T15:09:08+09:00" />
41+
<meta property="article:published_time" content="2025-02-08T03:03:00+09:00" />
4242
<meta name="twitter:card" content="summary" />
4343
<meta property="twitter:title" content="Categories" />
4444
<script type="application/ld+json">
45-
{"@context":"https://schema.org","@type":"BlogPosting","dateModified":"2025-02-05T15:09:08+09:00","datePublished":"2025-02-05T15:09:08+09:00","description":"우당탕탕 와르르멘션 개발일지","headline":"Categories","mainEntityOfPage":{"@type":"WebPage","@id":"http://localhost:4000/categories/"},"url":"http://localhost:4000/categories/"}</script>
45+
{"@context":"https://schema.org","@type":"BlogPosting","dateModified":"2025-02-08T03:03:00+09:00","datePublished":"2025-02-08T03:03:00+09:00","description":"우당탕탕 와르르멘션 개발일지","headline":"Categories","mainEntityOfPage":{"@type":"WebPage","@id":"http://localhost:4000/categories/"},"url":"http://localhost:4000/categories/"}</script>
4646
<!-- End Jekyll SEO tag -->
4747

4848

0 commit comments

Comments
 (0)