Skip to content

Commit 29d2abc

Browse files
docs: react-hook-form-1,2,3 수정
1 parent 63c2789 commit 29d2abc

File tree

3 files changed

+264
-242
lines changed

3 files changed

+264
-242
lines changed

contents/tech/react-hook-form-deep-dive-1/index.mdx

Lines changed: 45 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ slug: "/react-hook-form-deep-dive-1"
1919
> 분석에 사용한 RHF의 버전은 v7.53.0입니다.
2020
2121
## 비제어 컴포넌트 기반으로 구성된 RHF
22-
RHF는 Formik, Redux Form과 같은 다른 Form 라이브러리 대비 성능이 빠르다는 장점이 있습니다. 이러한 장점을 살리기 위해 RHF는 여러 기법을 사용하는데, 가장 근간을 이루는것이 바로 **비제어 컴포넌트**입니다. 따라서 RHF의 여러 기법이나 로직을 이해하기 위해서는 이에 대해 정확하게 이해해야합니다.
22+
RHF는 Formik, Redux Form과 같은 다른 Form 라이브러리 대비 성능이 빠르다는 장점이 있습니다. 성능이 빠르다는 장점을 위해 RHF는 여러 기법을 사용하는데, 가장 기본이 되는것이 바로 **비제어 컴포넌트**입니다. 따라서 RHF의 여러 기법이나 로직을 이해하기 위해서는 먼저 비제어 컴포넌트에 대해 정확히 이해해야합니다.
2323

2424
### 제어 컴포넌트 vs 비제어 컴포넌트
25-
비제어 컴포넌트는 React에서 Form을 사용하는 방법중 하나로, 흔히 제어 컴포넌트와 비교되는 개념입니다. 따라서 비제어 컴포넌트에 대해 정확하게 이해하기 위해서는 제어 컴포넌트에 대해서도 이해해야합니다. 두가지를 같이 살펴보겠습니다.
25+
비제어 컴포넌트는 React에서 Form을 사용하는 방법중 하나로 흔히 제어 컴포넌트와 비교되는 개념입니다. 따라서 비제어 컴포넌트에 대해 정확하게 이해하기 위해서는 제어 컴포넌트에 대해서도 이해해야합니다. 두가지를 함께 살펴보겠습니다.
2626

2727
제어 컴포넌트는 React의 상태를 이용하여 폼의 값을 관리하는것이고, 비제어 컴포넌트는 별도의 상태로 값을 저장하지 않고, 필요할때마다 html 요소에 저장되어있는 값을 가져오는 방식입니다. 아래의 예제코드를 살펴보시면 더욱 쉽게 이해할 수 있을것입니다.
2828

@@ -34,9 +34,6 @@ function ControlledComponent() {
3434

3535
const handleChange = (event) => {
3636
setValue(event.target.value);
37-
if(event.target.value.length> 10){
38-
setError(true)
39-
}
4037
};
4138

4239
const handleSubmit = (event) => {
@@ -79,7 +76,7 @@ function UncontrolledComponent() {
7976
);
8077
}
8178
```
82-
하지만 두 방식 모두 장단점이 존재합니다. 제어 컴포넌트는 입력값을 React의 상태로 관리하기 때문에 유효성검사, 포커스등의 효과를 즉각적으로 적용할 수 있다는 장점이 있으나, 값을 입력할때마다 리렌더링이 매번 발생한다는 단점이 있습니다. 반면 비제어 컴포넌트는 입력값이 변경될때마다 리렌더링이 발생하지는 않으나, 상태를 React에서 관리하지 않기 때문에 유효성검사, 포커스등의 효과를 적용하기 위해서는 추가적인 작업이 필요하기에 자칫 잘못하면 복잡하면서도 성능은 제어 컴포넌트와 유사한 컴포넌트가 탄생할수도 있다는 단점이 존재합니다.
79+
하지만 두 방식 모두 장단점이 존재합니다. 제어 컴포넌트는 입력값을 React의 상태로 관리하기 때문에 유효성검사, 포커스등의 효과를 즉각적으로 적용할 수 있다는 장점이 있으나, 값을 입력할때마다 리렌더링이 매번 발생한다는 단점이 있습니다. 반면 비제어 컴포넌트는 입력값이 변경될때마다 리렌더링이 발생하지는 않으나, 상태를 React에서 관리하지 않기 때문에 유효성검사, 포커스등의 효과를 적용하기 위해서는 추가적인 작업이 필요하기에 자칫 잘못하면 복잡하면서도 성능은 제어 컴포넌트와 유사한 컴포넌트가 될 수 있다는 단점이 존재합니다.
8380

8481
```typescript
8582
// 제어 컴포넌트
@@ -100,8 +97,8 @@ function ControlledComponent() {
10097
Name:
10198
<input type="text" value={value} onChange={handleChange} />
10299
</label>
103-
{/* value는 항상 최신 값이므로 조건만 부여해 렌더링합니다.*/}
104-
<span>{value.length>10?"길이가 너무 길어요":null}</span>
100+
{/* value는 항상 최신 값이므로 value에 조건만 부여해 렌더링합니다.*/}
101+
<span>{value.length>10 ? "길이가 너무 길어요" : null}</span>
105102
<button type="submit">Submit</button>
106103
</form>
107104
);
@@ -113,14 +110,9 @@ function UncontrolledComponent() {
113110
// 에러 관리를 위해 별도의 상태가 필요합니다.
114111
const [error, setError] = useState(false)
115112

116-
// 에러 관리를 위해서 input의 값이 변경될때 상태를 변경해주어야할수 있습니다.
113+
// 에러 관리를 위해서 input의 값이 변경될때 상태를 변경해야합니다.
117114
const handleChange = (event) => {
118-
if(event.target.value.length>10){
119-
setError(true);
120-
}else{
121-
setError(false);
122-
}
123-
115+
setError(event.target.value.length>10)
124116
};
125117

126118
const handleSubmit = (event) => {
@@ -134,15 +126,15 @@ function UncontrolledComponent() {
134126
<input type="text" ref={inputRef} onChange={handleChange}/>
135127
</label>
136128
{/* value가 항상 최신값을 반영하지 않으므로 별도 error상태를 바라보아야합니다*/}
137-
<span>{error?"길이가 너무 길어요":null}</span>
129+
<span>{error ? "길이가 너무 길어요" : null}</span>
138130
<button type="submit">Submit</button>
139131
</form>
140132
);
141133
}
142134
```
143-
두 방식의 장단점을 고려하였을때 어떤 것을 선택하더라도 성능 측면에서 유의미한 결과를 얻지 못할것이라는 생각이 들수도 있습니다. 하지만 RHF는 성능을 위해서라면 당연히 비제어 컴포넌트를 선택해야한다고 생각하였습니다. 왜냐하면 제어 컴포넌트의 경우 값이 변경되면 무조건 리렌더링이 발생해야하지만, 비제어 컴포넌트의 경우 값이 변경될때 리렌더링이 발생하지 않기 때문에, 에러, 포커스 등 폼 상태를 컴포넌트에 최신화 하는 과정에서 리렌더링을 제어 컴포넌트 만큼만 발생시키지만 않는다면, 최적화한것으로 볼 수 있기 때문입니다. 폼 상태를 항상 최신으로 유지하면서 최소한의 리렌더링을 사용하는 여러 아이디어들을 추후 보게될것입니다.
135+
두 방식의 장단점을 고려하였을때 어떤 것을 선택하더라도 성능 측면에서 유의미한 결과를 얻지 못할것이라는 생각이 들수도 있습니다. 하지만 RHF는 성능을 위해서라면 당연히 비제어 컴포넌트를 선택해야한다고 생각하였습니다. 왜냐하면 제어 컴포넌트의 경우 값이 변경되면 무조건 리렌더링이 발생해야하지만, 비제어 컴포넌트의 경우 값이 변경될때 리렌더링이 발생하지 않기 때문에, 에러, 포커스 등 폼 상태를 컴포넌트에 최신화 하는 과정에서 리렌더링을 제어 컴포넌트 만큼만 발생시키지만 않는다면, 최적화한것으로 볼 수 있기 때문입니다. 이와관련하여 폼 상태를 항상 최신으로 유지하면서 최소한의 리렌더링을 사용하는 여러 아이디어들을 추후 보게될것입니다.
144136

145-
한편 비제어 컴포넌트 기반으로 구성되어있다고 할지라도 필요에 따라 제어 컴포넌트를 폼요소로 사용할수 있습니다. 제어컴포넌트 지원이 필요한 이유는 MUI와 Ant와 같이 제어컴포넌트로 된 외부 UI 라이브러리와 RHF을 연결하는 경우가 있기 때문입니다. 이때는 Controller 컴포넌트를 이용하여 이를 쉽게 달성할 수 있습니다. 이 부분에 대해서는 마지막 아티클에서 살펴보게 될것입니다.
137+
한편 비제어 컴포넌트 기반으로 구성되어있다고 할지라도 필요에 따라 제어 컴포넌트를 폼요소로 사용할수 있습니다. 제어컴포넌트 지원이 필요한 이유는 MUI와 Ant와 같이 제어컴포넌트로 된 외부 UI 라이브러리와 RHF을 연결하는 경우가 있기 때문입니다. Controller 컴포넌트를 이용하여 이를 쉽게 달성할 수 있는데 이 부분에 대해서는 마지막 아티클에서 살펴보게 될것입니다.
146138

147139
## 자주 사용되는 유틸리티 함수
148140
RHF 내부에는 여러 유틸리티 함수들이 있습니다. 이들 중에는 `deepEqual`, `cloneObject`와 같이 이름만 보고도 어떤 동작을 하는지 예측가능한 함수가 있는 반면 `get`, `set`, `unset`과 같이 이름만으로는 동작을 예측하기 어려운 함수가 있습니다.
@@ -151,8 +143,8 @@ RHF 내부에는 여러 유틸리티 함수들이 있습니다. 이들 중에는
151143

152144
> 함수의 동작을 정확하게 이해하실수 있도록 코드 내부분석을 제공하지만, 사실 다음장을 읽기 위해 코드까지 이해할 필요는 없다고 생각합니다. 함수가 어떤 기능을 제공하는지만 이해해도 충분합니다.
153145
154-
### get, set, unset
155-
RHF에 html 요소를 등록할때 사용하는 name은 `"test"`같이 영문 또는 숫자로 구성하는 경우가 많은데, 경우에 따라서는`.`이나 `[]`를 이용하여 객체나 배열을 조회하는 형태로 입력할수 있습니다. 예를 들어
146+
### 객체 조작 함수
147+
RHF에 html 요소를 등록할때 사용하는 name은 일반적으로 `"test"`같이영문 또는 숫자로 구성된 단일 문자열인 경우가 많은데, 경우에 따라서는`.`이나 `[]`를 이용하여 객체나 배열을 조회하는 형태로 입력할수 있습니다. 예를 들어
156148
`"person.name.firstname[0]"` 과 같이 입력하고 필드에 `"테스트 입력값"`을 입력하면 다음과 같이 저장됩니다.
157149

158150
```json
@@ -164,7 +156,7 @@ RHF에 html 요소를 등록할때 사용하는 name은 `"test"`와 같이 영
164156
}
165157
}
166158
```
167-
따라서 RHF에서 객체를 조회, 변경, 제거할때는 일반적인 방법의 사용이 불가능힙니다. 왜냐하면 `formValue["person.name.firstname[0]"]`,`formValue["person.name.firstname[0]"]="변경된 테스트 입력값"`,`delete formValue["person.name.firstname[0]"]` 와 같은 코드를 통해서 해당 필드의 값을 조작할수 없기 때문입니다. 따라서 RHF는 이러한 형태의 name을 이용하여 객체를 조작하기 위해 객체를 조회, 변경, 제거하는 `get`, `set`, `unset`를 제공하고 있습니다.
159+
따라서 RHF에서 객체를 조회, 변경, 제거할때는 일반적인 방법을 사용할수가 없습니다. 왜냐하면 `formValue["person.name.firstname[0]"]`,`formValue["person.name.firstname[0]"]="변경된 테스트 입력값"`,`delete formValue["person.name.firstname[0]"]` 와 같은 코드를 통해서 해당 필드의 값을 조작할수 없기 때문입니다. 따라서 RHF는 이러한 형태의 name을 이용하여 객체를 조작하기 위해 객체를 조회, 변경, 제거하는 `get`, `set`, `unset`를 제공하고 있습니다.
168160

169161
#### get
170162
```typescript
@@ -267,7 +259,6 @@ export default function unset(object: any, path: string | (string | number)[]) {
267259

268260
return object;
269261
}
270-
271262
```
272263
첫번째 항목에서는 경로를 분해합니다. 이는 처음부터 `path`에 분리된 요소가 담긴 배열을 넣을수 있다는점을 제외한다면 앞선 `set` 함수에서 경로를 분리하는것과 동일합니다.
273264

@@ -276,7 +267,7 @@ export default function unset(object: any, path: string | (string | number)[]) {
276267
세번째 항목에서는 나머지 요소를 제거합니다. 이는 `paths`의 길이가 2이상일 경우 하나의 요소를 지웠을때 해당 요소가 빈객체 또는 빈 배열이면 상위요소도 지워주는 로직으로, 이를 위해 `path`를 하나 지운뒤 `unset`함수를 재귀적으로 호출합니다. 예를들어 `paths``["person","name","firstname"]` 인데, `object``name` 프로퍼티의 객체가 `firstname`하나의 프로퍼티만 가지고 있다면 `name` 객체가 삭제됩니다.
277268

278269
### 구독 관련 함수
279-
RHF에는 구독/발행 패턴을 사용할수 있도록 subject를 생성하는 함수`createSubject`, 이를 구독하기위한 `useSubscribe`가 존재합니다. 이들이 어떻게 구현되어있는지 살펴보고, 유즈케이스까지 살펴보겠습니다.
270+
RHF에는 구독/발행 패턴을 사용할수 있도록 subject를 생성하는 함수`createSubject`와 이를 구독하기위한 `useSubscribe`가 존재합니다. 이들이 어떻게 구현되어있고 어떻게 사용되는지 살펴보겠습니다.
280271

281272
#### createSubject
282273
```typescript
@@ -312,12 +303,11 @@ export default <T>(): Subject<T> => {
312303
};
313304
};
314305
```
315-
`createSubject`의 목적은 subject객체를 생성하여 반환하는것입니다.
316-
subject객체는 observer들을 저장해두고 변경이 발생할때마다 observer의 next 메서드를 실행하여 변경사실을 전파합니다. 이 subject에서 가장 중요한 두가지 메서드 next와 subscribe메서드를 자세히 살펴보겠습니다.
306+
`createSubject`의 핵심 목적은 subject객체를 생성하여 반환하는것입니다. subject객체는 observer들을 저장해두고 변경이 발생할때마다 observer의 next메서드를 실행하여 변경사실을 전파합니다. 이 subject에서 가장 중요한 두가지 메서드 next와 subscribe를 자세히 살펴보겠습니다.
317307

318-
next 메서드는 subject의 변경을 발생시키는 함수입니다. 이 함수가 실행되면 observer들의 next메서드를 실행하여 변경을 통보하는데, 이때 next 메서드를 실행할때 넘긴 인자를 같이 넘겨주어 변경된 값을 observer에서 알 수 있도록 합니다.
308+
next메서드는 subject의 변경을 발생시키는 함수입니다. 이 함수가 실행되면 observer들의 next메서드를 실행하여 변경을 통보하는데, 이때 next 메서드를 실행할때 넘긴 인자를 같이 넘겨주어 변경된 값을 observer에서 알 수 있도록 합니다.
319309

320-
subscribe 메서드는 observer를 subject에 구독 시키는 함수인데 observers에 observer를 추가하는 행위가 전부입니다. 그리고 반환할때 해당 observer을 unsubscribe하는 함수를 제공하여 구독 해제가 가능하도록 해줍니다.
310+
subscribe메서드는 observer를 subject에 구독 시키는 함수인데 observers에 observer를 추가하는 행위가 전부입니다. 그리고 반환할때 해당 observer을 구독해제하는 함수를 제공하여 구독을 해제할수 있도록 해줍니다.
321311

322312
#### useSubscribe
323313
```typescript
@@ -343,38 +333,38 @@ export function useSubscribe<T>(props: Props<T>) {
343333
useSubscribe는 subject에 observer를 좀더 편리하게 구독할수 있도록 도와주는 hook입니다. 앞서 보았을때 구독을 위해서는 subject에 observer을 넣고 실행하면 구독이 끝나는데 굳이 훅이 필요한가? 라고 생각하실수 있지만 disable 기능, useEffect를 사용해 구독과 구독 해제를 라이프사이클에 넣는 기능등의 역할을 제공합니다.
344334

345335
#### 실제 사용 케이스
346-
앞서 `createSubject``useSubscribe`를 살펴보았습니다. 이를 정확하게 이해하기 위해서는 실제로 어떻게 사용되는지 확인해보는것이 좋습니다.
336+
앞서 `createSubject``useSubscribe`를 살펴보았습니다. 이를 보다 정확하게 이해하기 때문에 사용사례를 살펴보겠습니다.
347337

348338
```typescript
349-
const _subjects: Subjects<TFieldValues> = {
350-
values: createSubject(),
351-
array: createSubject(),
352-
state: createSubject(),
353-
};
339+
const _subjects: Subjects<TFieldValues> = {
340+
values: createSubject(),
341+
array: createSubject(),
342+
state: createSubject(),
343+
};
354344
```
355-
위 로직은 creatFormControl내에서 subject를 생성하는 로직입니다.
345+
위 로직은 `createFormControl`내에서 subject를 생성하는 로직입니다.
356346

357347
```typescript
358-
useSubscribe({
359-
subject: control._subjects.state,
360-
next: (
361-
value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
362-
) => {
363-
if (
364-
shouldRenderFormState(
365-
value,
366-
control._proxyFormState,
367-
control._updateFormState,
368-
true,
369-
)
370-
) {
371-
updateFormState({ ...control._formState });
372-
}
373-
},
374-
});
375-
```
376-
`useForm` 내에서 `control``state`변화를 구독받기위해 사용합니다. next 콜백함수 본문에 대해서는 추후 살펴볼것입니다.
377-
```typescript
348+
useSubscribe({
349+
subject: control._subjects.state,
350+
next: (
351+
value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName }
352+
) => {
353+
if (
354+
shouldRenderFormState(
355+
value,
356+
control._proxyFormState,
357+
control._updateFormState,
358+
true
359+
)
360+
) {
361+
updateFormState({ ...control._formState });
362+
}
363+
},
364+
});
365+
```
366+
`useForm` 내에서 `control``state`변화를 구독받기위해 사용합니다. `next`프로퍼티 값인 콜백함수에 대해서는 추후 살펴볼것입니다.
367+
```typescript
378368
const _updateValid = async (shouldUpdateValid?: boolean) => {
379369
if (_proxyFormState.isValid || shouldUpdateValid) {
380370
const isValid = _options.resolver
@@ -392,7 +382,7 @@ const _updateValid = async (shouldUpdateValid?: boolean) => {
392382
위 코드는 formControl 내부에서 state subject를 변경시키기 위해 `_subjects.state.next`를 실행하는것입니다. 이 코드가 실행되면, useSubscribe의 next로 넘긴 콜백함수가 실행됩니다. 마찬가지로 `_updateValid`함수 내부 로직은 추후 살펴볼것입니다.
393383

394384
## 마치며
395-
RHF에 Deep Dive 할 준비는 끝났습니다. 다음 아티클에서는 useForm의 분석을 시작해보겠습니다.
385+
RHF에 Deep Dive 할 준비는 끝났습니다. 다음 아티클부터는 useForm을 분석해보겠습니다.
396386

397387
## 참고자료
398388
[[10분 테코톡] 세인의 제어 컴포넌트와 비제어 컴포넌트](https://www.youtube.com/watch?v=PBgQKK6nelo)

0 commit comments

Comments
 (0)