Hook Form으로 상태 관리하기.

jh_leitmotif·2022년 6월 18일
59
post-thumbnail

세상에? 왜 이렇게 조회수가 높은건지 모르겠다. 뭣 모르고 썼던 글이었는데, 여러모로 부끄럽다는 생각이 많이 들어서 글을 수정해본다. // 2023-01-25


React는 상태가 변화하면 리렌더링된다.

사용 중인 상태가 많지 않다면 큰 문제가 되지 않지만, 하나하나 추가되어가다 보면 렌더링 뿐만 아니라 특정 상태의 변화로 인해 의도하지 않은 사이드 이펙트가 일어날 때도 있다.

그래서 나는, 이런 리렌더링 이슈를 해결하고자 나온 비제어형 컴포넌트방식을 채택한 react-hook-form을 어떻게 사용하고 있는지를 소개한다.

react-hook-form

성능은 이 라이브러리가 만들어진 주된 이유입니다. React Hook Form은 ref를 이용한 비제어 컴포넌트방식을 이용해 어떠한 값을 입력할 때에 리렌더링의 횟수를 줄여줍니다. 이는 제어형 컴포넌트에 비해 빠른 마운트 속도를 보여줍니다.
// 인용 : https://react-hook-form.com/faqs

??? : 직접 Element를 제어하고 싶으면 useRef를 쓰세요!!

우리가 React를 처음 배울 때 심심찮게 보는 예제를 적어보면.

const [text,setText] = useState("");

return <input value={input} onChange={(e)=>setText(e.target.value)}/>

이런 코드가 있겠다.

아? React로 상태는 이렇게 관리하는구나~ 라고 넘어가보면, 한 웹페이지에 한개의 입력 창만 있는 것이 아님을 알게되고 상태를 어떻게 관리하지? 하고 고민하게 된다.

서울에 사는 김철수(가명)씨는 10개나 되는 input을 관리해야되서, 다음과 같이 코드를 짰다.

const [input1, setInput1] = useState("");
const [input2, setInput2] = useState("");
const [input3, setInput3] = useState("");
...
const [input8, setInput8] = useState("");
const [input9, setInput9] = useState("");
const [input10, setInput10] = useState("");

어... 영 정신 사납다. 하나의 상태로 묶어보자.

const [inputs, setInputs] = useState({
 input1:"",
 input2:"",
 ...
 input9:"",
 input10:"",
}

휴, 조금은 깔끔해졌다. 그런데 잠깐. input1을 업데이트 치려면 다른 input들도 값이야 변하지 않겠지만 계속 덮어씌워줘야 되는건가? 김철수씨는 눈 앞이 아득해졌다.

그리고 react-hook-form은 이런 문제를 다채롭게 해결한다.

register에 대하여.

왜 뜬금없이 register가 나왔느냐에 대해서는, 그냥 react-hook-form 코드를 보면서 내가 느낀 점을 서술하는 건데.

단순히 사용법만 보고 싶다면 이 단락은 건너뛰어도 좋다.

react-hook-form의 공식 홈페이지를 방문했을 때 아마도 맨 처음 보게될 register api의 예제를 보면.

<input {...register("firstName"}/>

또는...

const {onChange, onBlur, name, ref} = register("firstName");

<input 
 onChange={onChange}
 onBlur={onBlur}
 name={name}
 ref={ref}
/>

이런 예제를 보게 되는데, 특이하게 value를 가지고 있지 않다.

그 이유는 react-hook-form이 ref를 기준으로 설계되어있기 때문이다.

코드를 모두 가져오기엔 공간이 너무 작기 때문에, 나름 해석(?)한 것을 공유해보면.

https://github.com/react-hook-form/react-hook-form/blob/master/src/logic/getFieldValue.ts

직접 github에 올려져있는 소스를 살펴보면, getFieldValue 라는 스크립트가 정말 자주 쓰이는 것을 목격할 수 있는데 이 스크립트는 다음과 같은 역할을 수행하는 것으로 보인다.

 * 전달받은 ref의 타입을 확인해서 각 타입에 맞게 값을 추출한다.
  - input
  - radio
  - checkbox
  - multi select
  - file

그러니까 결국, ref.current.value와 다르지 않다라고 해석했다.

https://github.com/react-hook-form/react-hook-form/blob/master/src/logic/createFormControl.ts

그리고 createFormControl 스크립트의 register에서 onChange 함수가 어떻게 설계되어있나 살펴보면.

전달받은 event가 blur event라면 전달받은 onBlur 콜백을 수행하고.
그렇지 않다면 일반적인 input의 onChange와 같은 역할을 수행하는 것으로 보인다.

좀 더 깊게 보면 javascript의 Observer-Subscribe 패턴을 이용해 값을 관리하는 것으로 보인다.
이 패턴에 대해서는 정확하게 알지 못하기 때문에... 좀 더 공부하는 것으로 하고..

결론은. react-hook-form은 ref를 기준하여 API들이 설계되어 있다보니 직접 상태가 변경되는 것이 아니라
이미 메모리에 기억된 객체로부터 값을 꺼내온다고 해야되나.

마치 Global Store같은 역할을 한다고 느꼈고, 그래서 지금 나는 react-hook-form을 State Store같은 역할로 삼아 useState와 useEffect가 없는 프로그래밍을 지향하고 있다.


Example : 회원가입

간단한 회원가입 Form을 예로 들어보자.

useState를 쓰는 경우.

export default function InputStates() {
  const [userName, setUserName] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  return (
    <>
      <div>
        이름 :{" "}
        <input value={userName} onChange={(e) => setUserName(e.target.value)} />
      </div>
      <div>
        비밀번호 :{" "}
        <input
          type="password" value={password} onChange={(e) => setPassword(e.target.value)}
        />
      </div>
    </>
  );
}
export default function LandingPage() {
  const [gender, setGender] = useState<"M" | "F">("M");

  return (
    <div>
      <InputStates />
      <div>
        성별 :
        <label>
          <input
            type="radio" checked={gender === "M"} onChange={(e) => setGender("M")}
          />
          남자
        </label>
        <label>
          <input
            type="radio" checked={gender === "F"} onChange={(e) => setGender("F")}
          />
          여자
        </label>
      </div>
    </div>
  );
}

이름과 비밀번호는 같은 컴포넌트에 있기 때문에 둘 중 무언가가 입력되면 둘 다 함께 리렌더링된다.

그리고 성별은 최상위 컴포넌트에 자리하고 있기 때문에 버튼을 누를 때마다 모든 컴포넌트가 리렌더링된다.

하지만, React Hook Form의 경우

react-hook-form에서 입력 상태를 관리하는 api는 useForm이고, 기본적인 사용법은 아래와 같다.

import React from "react";

import { useForm } from "react-hook-form";

export default function LandingPage2() {
  const methods = useForm<{
    userName: string;
    password: string;
    gender: "M" | "F";
  }>({
    defaultValues: {
      userName: "",
      password: "",
      gender: "M",
    },
  });

  const { register } = methods;

  return (
    <div>
      <div>
        성별 :
        <label>
          <input type="radio" value={"M"} {...register("gender")} />
          남자
        </label>
        <label>
          <input type="radio" value={"F"} {...register("gender")} />
          여자
        </label>
      </div>
    </div>
  );
}

methods라고 표현한 것은 그냥.. 공홈에서 그렇게 쓰고 있어서. 맘대로 바꿔도 상관없다.

radio버튼은 value를 넣어준 것이 보이는데, 체크된 라디오 박스가 어떤 값인지 알아야 react-hook-form도 변경을 해주니, 당연한 처리라고 보면 된다.

위에 깜빡하고 텍스트박스에 대한 예제를 적지 않았는데, 아래와 같다.

<input {...register('userName')}/>

useState를 사용했을 때와 다르게, 아예 렌더링 표시가 나타나지 않는 것을 볼 수 있다.

register는 아마 맨처음 맟닥뜨리는 API 인데, 이 친구는 다음과 같은 것들을 반환한다.

const { onChange, onBlur, name, ref } = register("userName");

// 그래서... 다음과 같이 써도 상관 없다.

<input onChange={onChange} onBlur={onBlur} name={name} ref={ref}/>

// 혹은, onChange만 따로 가져가고 싶다면.

<input onChange={customOnChange} {...register('userName')}/>

마지막에 onChange만 따로 가져가는 예제를 올렸는데.

주의할 점은, react-hook-form의 onChange는 value가 아니라 ref, 즉 event 그 자체를 넘겨줘야 된다.

이 경우, customOnChange는 다음 코드처럼 기능하면 된다.

const customOnChange = (e) =>{
	const trimValue = e.target.value.replace(/-/g,'');
    const copyInput = document.createElement("input");
    copyInput.value = trimValue;

	onChange(copyInput)
}

나는 보통 한글만 입력받게 하고 싶다든지, 특정 문자를 없애고 싶다던지 하는 케이스에 대해 이렇게 쓰고 있다.

그렇다면 내가 변경시키는 값들이 진짜 변경되고 있는 걸까?
그를 위해서는 getValueswatch 를 사용하면 된다.

getValues

const {getValues} = methods;

useEffect(()=>{
 getValues("gender");
},[getValues("gender")]

getValues는 단순히 '값을 가져온다' 라고 해석하면 안된다. 실제 코드를 들여다보면 그냥 그 시점의 값을 땡겨오기만 한다.
즉, '호출하는 시점의 값을 가져온다' 라고 생각해야 된다.

실제로 위의 예제는 useEffect로 분명히 값이 변경되면 호출되도록 한 코드지만, 아무리 값을 변경해도 콘솔에 찍히는 것이 없다.

따라서 getValues는 사용시 개발자가 원하는 시점에 특정 값을 뽑아올 때에 쓰는 API라고 요약하면 될 것 같다.

watch

const {watch} = methods;
const gender = watch('gender')

console.log(gender);

선택된 성별은 {gender} 입니다.

watch API의 실제 코드를 들여다보면, observer pattern을 이용하고 있다.

https://dev.to/this-is-learning/rxjs-observer-and-subscription-5cg3

이 부분은 나부터 이해가 부족해서 좀 더 공부해야되는 부분인데.. 대충 생각해보면.
watch는 해당 필드의 이름으로 할당되어 있는 element를 구독해서 상태 변화를 감지해주는 API라고 할 수 있다.

그러나 공식 문서에서는

' watch는 호출될 때 어플리케이션 또는 Form을 전부 리렌더링하므로 성능 이슈가 발생한다면 별도의 콜백함수 또는 useWatch를 사용하세요 '

라고 지시하고 있다.

그동안 써온 코드를 되돌아보면, 실은 watch와 관련된 걸 쓴 적이 없다. 고작 디버깅할 때 값이 어떻게 변화하는지 보려고 할 때 썼던 것 같기도.

useWatch

const {register, control} = useForm.... ~

return(
	<Child control={control}/>
)


function Child({control}: { control: Control<Type, any> }) {
  const gender = useWatch({
    control,
    name: "gender",
  });

  return <>선택된 성별은 {gender} 입니다.</>;
}

useWatch는 ref의 변화를 감지해서 watch와 같은 역할을 수행하는데, useWatch 훅이 자체적으로 state를 가지고 해당 값을 반환해주는 역할을 하고 있기 때문에 전체 리렌더링은 발생시키지 않는 것이 아닐까.. 생각하고 있다.

물론, watch와 마찬가지로 이젠 쓰지 않고 있다.

setValue

register는 특정 규격 ( 텍스트, 라디오, 체크박스, 파일, 셀렉트 ) 에 맞는 API지만, 다양한 방식으로 입력 값이 업데이트 되어야 할 때가 많다. 이를테면 위에 서술했듯 한글 또는 영문, 숫자만 입력받는다던가.

그런 케이스를 위해 setValue라는 API가 존재한다.

const {control, setValue} = methods;

return (
	<Child control={control} setValue={setValue} />
)

function Child({
  control,
  setValue,
}) {
  const gender = useWatch({
    control,
    name: "gender",
  });

  return (
    <>
      <div>
        성별 :
        <label>
          <input type="radio" onChange={(e) => setValue("gender", "M")} checked={gender === "M"}
          />
          남자
        </label>
        <label>
          <input
            type="radio" onChange={(e) => setValue("gender", "F")} checked={gender === "F"}
          />
          여자
        </label>
      </div>
      선택된 성별은 {gender} 입니다.
    </>
  );
}

사용법은 단순히 setValue("필드명", 값) 으로 해주면 된다. 마찬가지로, 자체적으로 불필요한 리렌더링을 방지하도록 설계되어 있다고 설명되어 있다.

그런데 뭔가... 뭔가... 많다. 사실 이정도만 해도 useState를 떡칠하는 것보단 낫지만 뭔가.. 뭔가.. 더 좋은 해답이 없을까?

그런 생각이 들 때 쯤 내가 썼던 것이 Controller 였다.


Controller를 써보자.

Controller는 react-select, AntD, Material-Ui 등의 제어형 컴포넌트와 사용될 것을 염두에 두고 만들어진 컴포넌트다.

register, getValues, setValue.. 다 좋다 이거야. 이걸 하나로 합칠 순 없는거야? 라는 물음에 대한 답이라고 할 수 있다.

Controller

Controller는 name, control, render로 구성된다

name >> 가리킬 Form의 field 명
control >> useForm의 control 
render >> field에 의존하는 children Node

위의 코드를 실행시킨 결과는 아래와 같다.

위에 서술한 이것저것(?)들을 사용한 것과 똑같은 결과를 볼 수 있다.

controller의 field에서 빠져나오는 값은 register의 그것과 똑같고, value가 하나 추가되어 있는데.

아마 예상했겠지만, 우리가 익히 알고 있는

<input value={text}/>

이런 value를 뜻하는 것이 아니다.

render={({field}) =>{
	const {onChange, onBlur, value, ref, name} = field;
})

value는 useWatch를 이용해 해당 시점의 필드 값을 가져온 것으로 설계되어 있다.

코드를 보면 볼수록 이 라이브러리가 그만한 star를 받을 이유가 있다고 생각이 든다.

depth가 있는 값 구조는 어떻게?

이를테면,

defaultValues : {
	userInfo:{
    	userName:''
        password:''
        gender:''
    }
    
}

위와 같은 중첩된 Object 구조가 있을 때는 아래와 같이 대응한다.


  return (
    <>
      <div>
        <Controller
          name="userInfo.userName"
          control={methods.control}
          render={({ field }) => (
            <div>
              유저이름 :{" "}
              <input value={field.value} onChange={(e) => field.onChange(e.target.value)}
              />
            </div>
          )}
        />
        // ... 생략
      </div>
    </>
  );

Controller도 싫은 당신에게. useController.

사실은.

https://github.com/react-hook-form/react-hook-form/blob/master/src/controller.tsx

Controller는 위의 스크립트에서 빠져나오는데. 실제 코드는 그저 useController를 렌더링한다고 되어있다.

그래서 useController는 뭐하는 친구지? 라고 보면,

const { field } = useController({
 name:"userName",
 control:methods.control
})

const {name, value, onChange, onBlur, ref, formState, fieldState} = field;

이런식으로 나온다. 마치 register와 같지 않나? 싶다면 맞다.

formState, fieldState는 이 케이스에서 많이 사용하지 않다보니 (쓰더라도 isDirty정도를 쓴다) 이 문서에서는 다루지 않는다.

그래서 useController를 사용한 코드를 보면

const { field } = useController({
 name:"userName",
 control:methods.control
})

return <input onChange={field.onChange} value={field.value}/>

Controller 컴포넌트가 벗겨짐으로서 훨씬 가독성이 나아진 것을 볼 수 있다.

나아가보면, 이런식으로도 쓸 수 있다.


export default function useGetForms({control}){

const { field:userName } = useController({
 name:"userName",
 control:methods.control
})

const { field:userPhone } = useController({
 name:"userPhone",
 control:methods.control
})

const { field:userAddress } = useController({
 name:"userAddress",
 control:methods.control
})

return {userName, userPhone, userAddress};

}

커스텀 훅으로 useController를 한 데 밀어두고, field만 반환시켜서 좀 더 컨테이너와 로직을 분리할 수 있다.

const {userName, userPhone, userAddress} = useGetForms({control:methods.control});

return 
 <>
 	<input onChange={userName.onChange} value={userName.value}/>
    <input onChange={userPhone.onChange} value={userPhone.value}/>
    <input onChange={userAddress.onChange} value={userAddress.value}/>
 </>

이렇게 하나하나 쪼개다보면... 결국 useState 없는 구조를 만들 수 있다.


배열은 어떡해?

당연히 배열을 위한 API도 제공하는데, 우선 앞서 서술한 내용을 토대로 코드를 작성하면 아래와 같이 만들 수 있다.

defaultValue:{
	userArray:['a','b','c']
}

가령 위와 같은 값이 있다고 했을 때,

<Controller
 name='userArray'
 control={control}
 render={({field})=>(
   <>
	{field.value.map((item,index)=>(
    	<Controller
        	name=`userArray.${index}`
            control={control}
            render={({field})=>(
            	<>
                	<p> {field.value} </p>
                </>
            )}
        />
     )}
   </>
 }
/>

이렇게 작성하면 문제가 없기는...한데. Controller 안에 또 Controller라니. 좀 으악스럽다.

이것을 해결하는 훅이 바로 useFieldArray 되시겠다.

useFieldArray

필드에 배열 타입의 객체가 있을 때 사용해 코드의 양을 줄일 수 있다.

const {fields,append,prepend,remove,swap,move,insert}= useFieldArray({
  control,
  name:'userArray'
})

return(
  <>
      {fields.map((item,index)=>(
      	<Controller
          key={item.id}
          name=`userArray.${index}`
          control={control}
          render={({field})=>(
            	<input onChange={field.onChange} value={field.value}/>
          )}
        />
      )}
  </>
)

위와 같이 Controller 한 개를 줄이는 효과가 있고, 위의 예시처럼 그냥 단순한 input만 덩그러니 있는거라면.

<>
	{fields.map((item,index)=>(
    	<input key={item.id} {...register(`userArray.${index}`)}/>
    }
</>

이래버리면 된다.

useFieldArray에서 나오는 다른 콜백들은 다음과 같은 동작을 수행한다.

append: 뒤에 추가
prepend : 앞에 추가
remove : 해당 요소 삭제
swap : swap(from, to) 두 요소를 뒤바꾼다.
move : 지정된 위치로 이동
insert : 지정된 위치에 추가
update : 특정 위치의 요소를 업데이트. 해당 요소는 언마운트된 뒤 다시 마운트된다.
replace : 전체 배열요소를 대치

단. 한 가지 주의 사항이 있는데..

useFieldArray는 key 역할을 위한 id를 자동으로 생성해준다.

위의 예시 코드를 보면 심심찮게

key={item.id}

라는 코드를 봤을 텐데. 분명히 아까 우리가 만든 userArray에는 id라는 요소가 없다.

그렇다, 코드를 들여다보면 useFieldArray는 각 배열 요소에 대해 id를 자체적으로 생성해버린다.

https://github.com/react-hook-form/react-hook-form/blob/master/src/useFieldArray.ts

이 스크립트를 자세히 보면, 별도의 state에 field의 값을 담고 있고. 'generateId' 라는 자체 유틸 함수로 id를 생성하고 있다.

그래서 예를 들어서..

export default function useGetArray({control}){
	const {fields} = useFieldArray({
    	name:"userArray",
        control
    })
}

이렇게 커스텀 훅을 작성해두고서.

const {fields} = useGetArray({control});
// tempOne.tsx

const {fields} = useGetArray({control});
// tempTwo.tsx

이런식으로 각기 다른 컴포넌트에서 불러왔다고 했을 때.

이 두 fields는 같은 배열을 바라보고 있지 않는다.

사실상 useFieldArray가 2번 호출된 것이기 때문에 서로 초기 값은 같을 수는 있겠지만, 서로 다른 id를 지닌 다른 배열이 된다.

설상가상으로, 앞서 언급해둔 append, update, remove 등등의 함수들은 id를 기준으로 두고 동작하는 것으로 보인다.

그래서 아무리 tempOne에서 배열요소를 변경한다고 한들, tempTwo에서는 무용지물이 된다.

따라서 만약 react-hook-form을 store처럼 사용할 때 useFieldArray를 사용한다면

왜 값을 바꿨는데 안바뀌지?

와 같은 상황을 분명 한 번쯤은 마주칠 거라고 본다.

useFieldArray를 한 번만 호출하고서 그것을 props나 context 또는 별도의 store로 공유하면 되지만, 뭔가.. 뭔가... 싶다.

key를 id가 아닌 값으로 바꿀 수 있다.

const {fields} = useFieldArray({
 name:"userArray",
 control,
 keyName:"userKey",
})

keyName을 파라미터로 넘기면서 원하는 이름으로 넣으면 해당 이름으로 id가 생성된다.


여러 컴포넌트에 Form이 있으면 어떡해?

Context API로 값을 자식 컴포넌트에 내려본 적이 있는 리액트 개발자라면, 아마 1분만에 이해하고 사용할 것이다.

react-hook-form은 FormProvider와 useFormContext를 제공한다.

사용법은 아래와 같다.


const methods = useForm({
	defaultValues:{
    	tempVar:"Hello world!"
    }
});

return (
 <FormProvider {...methods}>
  <ChildComponent/>
 </FormProvider>

// Parent.tsx
const {control, getValue, setValue, formState.....} = useFormContext()

const {field:tempVar} = useController({
 name:"tempVar"
 control
});

const methods = useForm({
	defaultValues:{
    	childVar:"",
    }
})

const {field:childVar} = useController({
 name:"childVar",
 control:methods.control
})

return (
	<> 
		<input onChange={tempVar.onChange} value={tempVar.value} />
        <input onChange={childVar.onChange} value={childVar.value} />
	</>
	
)

// ChildComponent.tsx

심플하다. 부모의 Form을 공유하고자 하는 자식 컴포넌트를 react-hook-form에서 제공하는 FormProvider로 감싸서 methods 자체를 내려주고, 자식 컴포넌트에서는 useFormContext로 그것을 전달받기만 하면 된다.

그와 동시에 자식 컴포넌트에서 useForm을 호출해서 사용해도 된다.

주의할 점은 useFormContext는 가장 가까운 부모의 useForm을 가져오게 된다.

벤다이어 그램을 생각하면 될 것 같은데...

B에서 useFormContext를 쓰면 A의 것을.

C에서 쓰면 B의 것을 가져온다고 보면 될 것 같다.

유용한 기능이지만, context를 많이 만들거나 props가 과다해지는 현상을 생각해서
되도록 useFormContext를 한 번만 쓰는 2 Level 정도로만 컴포넌트 깊이를 유지하려고 노력하고 있다.

내가 react-hook-form을 사용하는 방법

끝으로, 요즘 회사에서 코드치면서 react-hook-form을 사용하는 방법을 소개하는 것으로 마무리하려고 한다.

export default function useGetUserForms({control}){
  const { field:userName } = useController({
   name:"userName",
   control:methods.control
  })

  const { field:userPhone } = useController({
   name:"userPhone",
   control:methods.control
  })

  const { field:userAddress } = useController({
   name:"userAddress",
   control:methods.control
  })

  return {userName, userPhone, userAddress}; 
}
const methods = useForm({
 defaultValues;{
  userName:"",
  userPhone:"",
  userAddress:"",
 }
});

const {userName, userPhone, userAddress} = useGetUserForms({control:methods.control});

return 
 <>
 	<input onChange={userName.onChange} value={userName.value}/>
    <input onChange={(e)=>phoneHypenChange({e,onChange:userPhone.onChange})} value={userPhone.value}/>
    <input onChange={userAddress.onChange} value={userAddress.value}/>
 </>

// UserInputForm.tsx
export default function useGetAgreeInfoArray({control}){
 const {fields:agreeInfo} = useFieldArray({
 	name:"agreeInfo",
    control,
    keyName:"_id"
 })
 return {agreeInfo}
}
const methods = useForm({
 defaultValues:{
  agreeInfo:[]
 }
);

const {agreeInfo} = useGetAgreeInfoArray({control:methods.control});
const {data} = useGetAgreeInfo({resetField:(value)=> methods.resetField("agreeInfo",{defaultValue:value}));

return (
	<>
    	{ agreeInfo.map((i, idx)=>{
        	<div key={i._id}>
            	{ i.agreeContent }
            </div>
          })
        }
    </>
)

// AgreeInfo.tsx
export default function SignUp(){
  return (
   <UserInputForm/>
   <AgreeInfo/>
  )
}
// SignUp.tsx

단순히 회원정보를 입력하는 컴포넌트와 약관 정보를 읽는 컴포넌트로 구성한다.

약관 정보를 서버로부터 받아오면서 agreeInfo 배열에 약관 데이터를 담는다.

즉석으로 글을 작성하며 코딩한 것이라 submit을 안적었는데, 아마도 UserInputForm.tsx 컴포넌트에 별도로 버튼을 만들어서 그 쪽에 submit을 할 수 있도록 할 것 같다.

요약하면.

  1. useController나 useFieldArray는 커스텀 훅으로 숨긴다.
  2. 각 입력 값은 각 관심사별로 컴포넌트를 나눈다.
  3. 실제로 보여주는 컨테이너에는 단 1개의 로직이나 상태, 또는 값 없이 컴포넌트만 반환한다.
  4. 이 과정 속에서 useState와 useEffect는 불가피한 것이 아니라면 사용하지 않도록 한다.

단점은 너무 추상화된다는 점, 그리고 코드 설계를 단단하게 가져가지 않으면 꽤나 값 변경을 추적하는 데에 어려움을 겪는다는 점이 있다.

그래서 왠만하면 처음부터 구조를 잘 잡고, 또 주석으로 잘 코드를 트레이싱할 수 있도록 하려고 노력하고 있다.


끝!

대강 내가 쓰는 react-hook-form 방식을 써둔 레포지토리가 있다.

https://github.com/KimJeongHyun/react-hook-form-test

프로토타입 형태로 작성해둔 것이다보니, 기능적으로 보기보다는 이 사람은 react-hook-form을 이렇게 쓰는구나.. 정도로만 참고해준다면 좋을 것 같다.

한편으로는 요즘엔 너무 단단하게 결합되어버려 react-hook-form이 강제되는 구조가 아닐까 하는 생각을 하고 있다보니, 입력에서 벗어나 다른 분야 쪽의 코드로 환기하려고 노력하고 있는 와중이다.

profile
Define the undefined.

7개의 댓글

comment-user-thumbnail
2023년 1월 10일

감사합니다.

답글 달기
comment-user-thumbnail
2023년 5월 5일

controller 사용법을 찾아 헤매다 여기까지 오게됐습니다 감사합니다.

답글 달기
comment-user-thumbnail
2023년 5월 18일

많은 도움이 되었습니다. 감사합니다.

답글 달기
comment-user-thumbnail
2023년 6월 18일

잘봤습니다!

답글 달기
comment-user-thumbnail
2024년 5월 1일

Shadcn-ui에서 react-hook-form과 zod를 이용한 form validation 방법을 소개하고 있는데, 어떤 방식으로 동작하는지 이해하는데 이 글이 도움이 많이 되었습니다.
1. react에서 controlled input을 직접 관리하게 되면 중복 코드가 많이 생생되고 다른 input의 rerendering을 유발하여, react-hook-form의 register를 사용할 수 있다.
2. react-hook-form의 register를 사용하면 getValue를 통한 현재 값 확인, deeply nested object에 대한 form 상태 관리가 불편해진다. 특히 uncontrolled component로 설계되어 나오는 MUI 사용하게 된다면 귀찮아지는데, Controller와 control을 사용하면 이를 해결할 수 있다.

1개의 답글
comment-user-thumbnail
2024년 5월 1일

Shadcn-ui에서 react-hook-form과 zod를 이용한 form validation 방법을 소개하고 있는데, 어떤 방식으로 동작하는지 이해하는데 이 글이 도움이 많이 되었습니다.
1. react에서 controlled input을 직접 관리하게 되면 중복 코드가 많이 생생되고 다른 input의 rerendering을 유발하여, react-hook-form의 register를 사용할 수 있다.
2. react-hook-form의 register를 사용하면 getValue를 통한 현재 값 확인, deeply nested object에 대한 form 상태 관리가 불편해진다. 특히 uncontrolled component로 설계되어 나오는 MUI 사용하게 된다면 귀찮아지는데, Controller와 control을 사용하면 이를 해결할 수 있다.
로 이해했습니다!

답글 달기