- Published on
react-hook-from 에 대한 고찰
- Authors
- Name
- 길재훈
react-hook-form 에 대한 고찰
react-hook-form 을 사용하면서 그저 uncontrolled component 이다. 정도로만 사용했다.
사실, 그보다는 hooks 로 빼서 처리할 수 있다는 굉장함에 많이 사용했던것 같다.
react-hook-form 이라는 라이브러리를 사용하면서 느낀 편리함보다,
이러한 라이브러리가 왜 만들어 졌으며, 이로인해 얻을수 있는 이득은 무엇일까 라는
의문이 들었다.
이는 내가 나름 찾아보고, react-hook-form(input) 을 어떻게 사용해야 더 잘 사용할지에 대해 정리한 내용이다.
uncontrolled component
Less code, More performant
react-hook-form docs 에 제일먼저 등장하는 문구이다.
그러면서 다음과 같은 설명이 나온다.
react-hook-form은 불필요한re-render을 제거하면서, 코드의 양을 줄여준다.
이러한 방식을 가능하게 해주는 것이 바로 uncontrolled component 이다.
controlled component 와 uncontrolled component
controlled component 는 일반적으로 useState 를 사용하여,react 자체에서 state 를 controll 할 수 있는 component 를 말한다.
controlled component 의 예
interface IValues {
email: string
password: string
}
const testForm = () => {
const [values, setValues] = useState<IValues>({
email: '',
password: '',
})
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
alert(values)
}
const onChangeValues = (e: ChangeEvent<HTMLInputElement>) => {
setValues((prev) => ({
...prev,
[e.target.name]: e.target.value,
}))
}
return (
<form onSubmit={onSubmit}>
<label>
email:
<input name="email" value={values.email} onChange={onChangeValues} />
</label>
<label>
password:
<input type="password" name="password" value={values.password} onChange={onChangeValues} />
</label>
<button>완료</button>
</form>
)
}
그렇다면, uncontrolled component 란 state 로 작동을 안한다는 말인가?
맞다. uncontrolled component 는 말 그대로 react 에서 controll 하지 않는다.
uncontrolled component 는 DOM 에 접근하여 값을 받아온다.
uncontrolled component
interface IValues {
email: string
password: string
}
const testForm = () => {
const emailRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
const emailValue = emailRef.current.value
const passwordValue = passwordRef.current.value
alert(`emailRef: ${emailValue}, passwordRef: ${passwordValue}`)
}
return (
<form onSubmit={onSubmit}>
<label>
email:
<input name="email" ref={emailRef} />
</label>
<label>
password:
<input type="password" name="password" ref={passwordRef} />
</label>
<button>완료</button>
</form>
)
}
위의 code 를 보면 알겠지만, DOM 에 접근하여 처리된 값을 받아오기 위해 ref 를 사용해서,
값을 받아오는것을 볼 수 있다.
근데, DOM 에서 받아오는것(uncontrolled)과, state 를 통해 처리(controlled)하는것과 무엇이 다른데?
이를 명확하게 이해하기 위해서는 State Colocation 에 대한 이해가 필요하다.
State Colocation(상태 공존)
원글은 state-colocation-will-make-your-react-app-faster 에서 참고하였다.
State Colocation 은 Global State 가 얼마나 좋지 않은지 알려주며,Application 이 더 빠르게 작동할 수 있도록 해주는 방법이다.
해당 글을 본다면, State Colocation 개념은 은 RE-Render 되는 react 에서 얼마나
중요한지 설명해준다.
다음을 보도록 하자.
const SlowComponent = ({value, onChange}: {value: string, onChange: (e: ChangeEvent<HTMLInputElement>: void)}) => {
...오래걸리는 작업
return (
<label>
오래걸리는 작업:
<input type="text" value={value} onChange={onChange}/>
</label>
)
}
const TestComponent = ({slow, value, onChange}: {slow: string, value: string, onChange: (e: ChangeEvent<HTMLInputElement>: void)}) => {
return (
<label>
오래걸리는 SlowComponent 의 slow 값을 사용하는 Input:
<input type="text" value={value} onChange={onChange}/>
<span>`slow값은 ${slow}에요.`</span>
</label>
)
}
const AppComponent = () => {
const [value, setValue] = useState('')
const [slow, setSlow] = useState('느리다')
const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}
const onChangeSlow = (e: ChangeEvent<HTMLInputElement>) => {
setSlow(e.target.value)
}
return (
<TestComponent
slow={slow}
value={value}
onChange={onChangeValue}
/>
<SlowComponent
value={slow}
onChange={onChangeSlow}
/>
)
}
위 내용은 해당 blog 의 내용을 참고해서 이해한 내용을 정리한 code 이다.
SlowComponent 는 어떠한 작업이 오래걸리는 component 를 말한다.
여기서 중요한것은 SlowComponent 에서 변경하는 slow 값을 TextComponent 에서props 로 전달받아 사용한다는 것이다.
만약, SlowComponent 가 onChangeSlow 를 실행하는데 1sec 이 걸린다고 가정해보자. 그리고 TestComponent 의 input 에 글을 작성한다고 가정해보자.
TestComponent 의 input 은 곧바로 잘 작성될까?
아니면, 1sec 이후에 input 이 작성될까?
결과는, 1sec 이후에 input value 가 작성된다.
이는 굉장히 재미있는 상황이다.TestComponent 는 SlowComponent 의 오래걸리는 작업 의 영향을 받아서, value 작성이 오래걸리는 작업(1sec) 만큼 시간이 걸리는 상황이 발생한 것이다.
이는, react 의 re-render 와 연관있다.
react 는 Component re-rendering 하는 조건이 몇가지가 존재한다
State가 변경될때- 부모 컴포넌트가 다시 랜더링될때
- 새로운
Props가 들어올때 Props가 변경될때
지금 상황은 각 Component 가 부모 Component 에 존재하는 state 값을 변경한다.
그렇다는건,오래걸리는 작업(1sec) 을 가진 SlowComponent 도 input 의 state가
변경될때 마다 re-rendering 된다는 것이다.
input 하나 변경하겠다고, 모든 Component 가 다시 그려지는, 굉장히 비효율적인
상황이 발생한 것이다.
그래서 위의 참고한 Blog 에서는 이에 대한 해결책으로 State Colocation 을 강조하며,
컴포넌트단위로 state 를 생성하여, re-rendering 되는 현상을 없애고, 효율적으로 관리하라고 설명해준다.
이는 다음과 같다.
const SlowComponent = ({value, onChange}: {value: string, onChange: (e: ChangeEvent<HTMLInputElement>: void)}) => {
...오래걸리는 작업
return (
<label>
오래걸리는 작업:
<input type="text" value={value} onChange={onChange}/>
</label>
)
}
const TestComponent = ({slow}: {slow: string}) => {
const [value, setValue] = useState('')
const onChangeValue = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}
return (
<label>
오래걸리는 SlowComponent 의 slow 값을 사용하는 Input:
<input type="text" value={value} onChange={onChange}/>
<span>`slow값은 ${slow}에요.`</span>
</label>
)
}
const AppComponent = () => {
const [slow, setSlow] = useState('느리다')
const onChangeSlow = (e: ChangeEvent<HTMLInputElement>) => {
setSlow(e.target.value)
}
return (
<TestComponent
slow={slow}
value={value}
onChange={onChangeValue}
/>
<SlowComponent
value={slow}
onChange={onChangeSlow}
/>
)
}
위의 코드는TestComponent 의 state 값이 변경될때, re-rendering 되지 않도록TestComponent 내부에 state 를 선언하여 처리하는 logic 이다.
이제, TestComponent 의 Input 값 변경시, 부모 컴포넌트에 위해 전체 re-rendering 되는 상황은 없어졌다.
이것이 바로, State Colocation 의 장점이다.
그래서, Uncontrolled Component 와 무슨 상관인데?
다시, Controlled Component 의 Code 로 되돌아 가보자.
controlled component 의 예
interface IValues {
email: string
password: string
}
const testForm = () => {
const [values, setValues] = useState<IValues>({
email: '',
password: '',
})
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
alert(values)
}
const onChangeValues = (e: ChangeEvent<HTMLInputElement>) => {
setValues((prev) => ({
...prev,
[e.target.name]: e.target.value,
}))
}
return (
<form onSubmit={onSubmit}>
<label>
email:
<input name="email" value={values.email} onChange={onChangeValues} />
</label>
<label>
password:
<input type="password" name="password" value={values.password} onChange={onChangeValues} />
</label>
<button>완료</button>
</form>
)
}
이 code 의 문제점은 State Colocation 을 설명하면서 발생한 문제와 같다.
Input 하나 변경하면, State 값이 변경되므로, 해당 컴포넌트가 전체 Re-Rendering 되는 상황이다.
물론, Input 이 몇개 되지 않는다면, 별 문제가 없겠지만, 대형 서비스 같은경우에는input 사용이 1000개 가까이 이루어진다는 글을 본적이 있다.
이러한 상황이면 1000개가 re-rendering 된다는 것이다.
만약, State 를 통한 Input 조작이 아니라, DOM 을 통해 접근(Uncontrolled Component)한다면,State 를 통하지 않았으므로 이러한 쓸데없는 Re-rendering 이 발생안하지 않을까?
바로 이러한 관점에서 react-hook-form 은 uncontrolled Component 를 통해,input 값을 수정하는 방식을 사용한다.
이를 말해주는 부분은 Docs 에서 잘 보여주고 있다.
Controlled Component 일때, 각 Component 의 움직임이 남다르다
이를 통해 react-hook-form 이 왜 Uncontrolled Compoennt 방식을 사용하여,Input 을 처리하는지에 대한 개념을 알게 되었다.
하지만, Controlled Component 를 사용해야만 할때가 있다면...?
예를 들어, React-Select, MUI, AntD 같은, 미리 작성된 UI Component 를 가져올때 controlled component 로 작성되어 있다면, 어떻게 처리해야할까?
react-hook-form 은 이러한 미리 작성된 Controlled Component 역시 같이 관리가능하도록 해당 API 를 제공한다.
Controller
Controller 는 controlled component 를 wrapping 해서, react-hook-form 에서 같이 관리할 수 있도록 만들어주는 api 이다.
docs 에서는 이 api 를 다음과 같이 사용하라고 말한다.
<Controller
control={control}
name="test"
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, isTouched, isDirty, error },
formState,
}) => (
<Checkbox
onBlur={onBlur} // notify when input is touched
onChange={onChange} // send value to hook form
checked={value}
inputRef={ref}
/>
)}
/>
각 Props 에 대해서는 다음의 Docs 를 참고하자.
여기서 중요한 부분은, control 과 render 부분이다.
control 은 docs 에서 다음처럼 설명하고 있다.
이
Object는Component를react-hook-form에 등록하기 위한 메서드를 포함하고 있다.
control 을 불러올때, useForm 을 사용하여 가져오는데, 해당 type 을 살펴보니, 정말 내부적으로 사용하는 code 가 가득했다.
export type Control<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
_subjects: Subjects<TFieldValues>
_removeUnmounted: Noop
_names: Names
_state: {
mount: boolean
action: boolean
watch: boolean
}
_reset: UseFormReset<TFieldValues>
_options: UseFormProps<TFieldValues, TContext>
_getDirty: GetIsDirty
_resetDefaultValues: Noop
_formState: FormState<TFieldValues>
_updateValid: (shouldUpdateValid?: boolean) => void
_updateFormState: (formState: Partial<FormState<TFieldValues>>) => void
_fields: FieldRefs
_formValues: FieldValues
_proxyFormState: ReadFormState
_defaultValues: Partial<DefaultValues<TFieldValues>>
_getWatch: WatchInternal<TFieldValues>
_updateFieldArray: BatchFieldArrayUpdate
_getFieldArray: <TFieldArrayValues>(name: InternalFieldName) => Partial<TFieldArrayValues>[]
_executeSchema: (names: InternalFieldName[]) => Promise<{
errors: FieldErrors
}>
register: UseFormRegister<TFieldValues>
unregister: UseFormUnregister<TFieldValues>
getFieldState: UseFormGetFieldState<TFieldValues>
}
control 객체는 react-hook-from 에서 입력 필드를 추적하고 관리하는데 사용된다.
render 부분은 React 에서 제공하는 render prop 을 사용하여 처리한다. 이는 비표준 props 를 가진 외부 controlled component 와의 통합을 단순하게 만들어준다.
즉, render 에 사용되는 자식 컴포넌트에 onChange, onBlur, name, ref, value 를 제공한다.
render 시 제공되는 props 는 다음의 순서를 갖는다.
field,fieldState,formState
이 값들을 가진 객체를 통해서 controleld component 관리가 이루어진다.
그렇다면, Controller 를 hooks 로 뺄수는 없을까?
useController
useController 는 react hook form 에서 정고하는 hooks 중 하나이다. 이는 controlled component 를 관리하기 위해 사용된다.
간단하게 controller 의 hooks 버전으로 생각하면 된다.
api 에 대해서는 다음의 useController Docs 에서 살펴보자
code 는 이러하다.
import React from 'react'
import { TextField } from '@material-ui/core'
import { useController, control } from 'react-hook-form'
function Input({ control, name }) {
const {
field: { ref, ...inputProps },
fieldState: { invalid, isTouched, isDirty },
formState: { touchedFileds, dirtyFileds },
} = useController({
name,
control,
rules: { required: true },
defaultValue: '',
})
return <TextField {...inputProps} inputRef={ref} />
}
function App() {
const { control } = useForm()
return <Input name="firstName" control={control} />
}
이는 hooks 로 변경되었을 뿐 Contorller 와 동일한 api 를 가진다.
마무리
react-hook-form 을 왜 사용하는지 알기 위해 uncontrolled component 와 controlled compoennt 에 대해서 알아보았으며, 그로인해 State Colocation 이 어떻게 발생하는지도 알아보았다.
이러한, 지식을 가지고 접근한다면, 추후 react 를 사용하여 application 을 만들때,
많은 도움이 될것으로 생각이 든다.
그저, 많이 사용하니까 가 아닌 왜 사용하는지 를 아는 중요한 계기가 된것 같다.
앞으로 library 에 대해서 더 많이 알아가면서, 이러한 개념적 토대를 기반으로 한다면,library 가 만들어지게된 이유에 맞추어 code 를 작성하기 좋을 것이다.
react-hook-form 은 정말 멋진 libray 이다.