맨날 하는 폼, 좀만 더 쉽게 해보자 React-hook-form

hdpark·2022년 5월 3일
51
post-thumbnail

Form은 지겹다

프론트엔드 개발을 한 번이라도 해봤다면 단순한 Input을 시작으로 많은 수의 폼을 다루게 된다.
프론트엔드 개발자가 다뤄야 하는 폼의 형태는 매우 다양하고, 비즈니스 로직과 백엔드에서 요구하는 로직을 성공적으로 구현해야 한다. 물론 유저가 폼을 입력했을 때의 만족감 또한 놓쳐서는 안되는 중요한 파트이기도 하다.

하지만 여러가지 폼을 개발하다 보면 점점 더 복잡해지는 비즈니스 로직과 개발자 경험 사이에서
고민하고 있는 시간이 길어짐을 발견하기 시작한다.

하나의 Input으로 모든 로직을 처리하게 만들었다 생각했지만 기획자의 새로운 기획서 한 장에 원본과 비슷한 Input2를 만들고 있다던가, props 누더기가 되어버린 컴포넌트를 보고 있으면 왜 이 단순한 텍스트 쪼가리를 보내는 게 아직도 이렇게 복잡해야 하는지 강렬한 회의감이 밀려온다.

브라우저마다 표준도 잡히지 않는 Select-Option을 구현하는 것도 그렇고 정규식을 포함한 에러 메시지처리까지. 이전 프로젝트에서 사용된 코드를 다음 새로운 프로젝트에 사용하는 것 또한 녹록치 않다.

여러 프로젝트에서 데이터를 가공하는 함수를 두루 사용한 경험은 있지만 유독 폼에 관련된 코드들은 재사용성이 매우 떨어졌다. 특히 리액트에서 state로 폼의 데이터를 관리함에 따라 얼마만큼의 범위를 하나의 로직으로 지정하느냐 역시 새로운 고민을 낳는다.

고민도 고민이었지만.. 결론적으로 폼은 나에게는 매우 지겹고 재미없는 작업이었다🙄

왜 지겨울까?

고전적인 방법으로 하나의 인풋에 접근해서 값을 가져오기 위해서는 HTMLElement를 지정하여
value를 가져오는 것만으로도 충분했다.

하지만 리액트에서는 state로 값을 바인딩해서 DOM에 직접 접근하지 않고
화면의 업데이트와 인풋의 데이터를 다룬다.

 	const [value, setValue] = useState("")
	<input type="text" value={text} onChange={e => setValue(e.target.value)} />

함수 형태의 컴포넌트를 사용한다면 단순하게 useState를 사용하여 인풋을 컨트롤하게 된다...만,
아쉽게도 위와 같은 단순한 인풋은 거의 사용되는 일이 없다

보통 2-3개의 인풋이 하나의 페이지, 또는 컴포넌트에서 동시에 사용되고
그 인풋들은 보통 다른 방식으로 작동될 가능성이 크다.

	const handleText = (e) => {
      const {value} = e.target
      // ... 뭔가의 로직
      setText(newValue)
    }
    
    const handleName = (e) => {
      const {value} = e.target
      // ... 위와 다른 뭔가의 로직
      setName(newValue)
    }
    
    <input type="text" value={text} onChange={handleText} />
    <input type="text" value={name} onChange={handleName} />

자, 이제 인풋은 두개가 되었고 이를 다룰 함수도 두개가 되었다.
handle함수들은 내부에서 서로 각기 다른 복잡한 로직을 수행하고
서로 다른 성격의 인풋이 많아지면 많아질수록 handle하는 함수의 수는 계속 늘어난다.

이를 해결하기 위해서 모든 인풋을 소화할 수 있는 정교한 hooks나 함수를 만들면 되지 않을까?
라는 발상으로 handleUpdate함수를 만들었지만, 결국 코드를 수정하는 속도는
기획서를 수정하는 속도를 따라갈 수 없다는 사실만을 재확인할 수 있었다😊

폼은 대부분의 생김새도, 작동방식도 비슷하지만 로직만은 애매하게 달라야 하는 부분이 생기기 마련이다.
검은색 선이 쳐진 흰색 박스 안에 글씨가 한글자씩 쳐질 뿐이라 시각적으로 별 감흥도 없는데도
구현하는데에는 까다로운 작업들을 요구한다. 그래서 폼은 지겹다.

오늘도 조금만, 조금만 더 쉽게

역시나 이런 방식의 폼을 다루는 권태감은 다른 개발자들도 공감하는 부분일 것이다.
반복되는 state를 다루는 작업에서 파생된 고수들의 hooks나 방법론이 스택 오버플로우에도 넘쳐난다.
하지만 이런 와중에도 대세가 되는 패키지가 하나 있었는데 그게 바로 오늘 얘기할 React Hook Form 이다.

React-hook-form은 이름에서도 드러나듯이 폼을 다루는데에 매우 유용한 hooks 묶음들을 제공한다.
각 인풋을 핸들하는 함수부터, submit을 처리하고 오류를 잡아내는 방식,
에러 처리와 state를 추가로 가공하는 방식들이 굉장히 직관적이고 세련된 방식이라
작업하면서 앞으로 폼을 다룰 때 사용할 필수 패키지라고 생각한다.

일단 간단한 사용법을 보자.

	import { useForm } from "react-hook-form"
    
  	const Page = () => {
      // 여타 hooks처럼 useForm() 함수에서 필요한 함수들을 리턴받는다 
      const { register, handleSubmit } = useForm()
      
      
      const onSubmit = data => {
        // submit이 발동되면 자동으로 form태그 안의 데이터를 객체로 깔끔하게 가져온다
      	console.log(data)
      }
      
      return (
          <form onSubmit={handleSubmit(onSubmit)}>
              <input {...register("email")} />
			  <input {...register("name")} />
              <input {...register("address")} />
              <input {...register("cvc")} />
              .
              .
              . 
			  .
              <input {...register("latte")} />
              <button type="submit" />
          </form>
      )
  }		

코드의 양부터 벌써 React-hook-form의 강점이 보인다. 일단 register를 불러오기만 하면
한 화면에 인풋이 아무리 많아도 register 함수 하나로 전부 처리된다.
useState로 구현하면 쓸데없이 많은 state를 생성해야 하거나
커다란 객체를 생성해서 그 객체를 다루는 무거운 함수를 만들어야 한다. 상상만 해도 끔찍하다

또 인풋이 많아지면 많아질수록 백엔드로 보내야 하는 객체도 커질 것이고
객체안에 값을 담는 것도 귀찮은 작업이 된다.
하지만 handleSubmit에 onSubmit함수를 만들어서 넣어주면
data안에 인풋의 모든 데이터를 keyValue 페어로 깔끔하게 흘려보내 준다!

이제 압박이었던 폼들의 state에서 한걸음은 해방되었다

🙁 그래서 { ...register( " state " ) } 가 뭔데

'input에 { ...register("name") } 을 풀어넣는 것만으로도 어떻게 구현이 될까' 가 맨처음 느낀 가장 큰 궁금증이었다. 그래서 콘솔에 찍어 넣었더니,


요런 놈이 나왔다. input에서 요구하는 attribute를 만들어서 자동으로 짜넣고 있었다.
게다가 리액트라는 이름을 달고 나온 것 처럼 ref까지 제공해준다.
(물론 Ref를 사용하면 사용법이 약간 달라지긴 한다.)

이제 register가 대신 state를 업데이트 시켜주는 역할을 한다는 것은 알았다.
그렇지만 기획자가 나를 참조한 기획서의 허들을 충족시켜주지는 못할 것은 당연한 일!

당연히 유효성 체크를 넣어야 하고 복잡한 정규식을 때로는 만족시켜야 한다는 것을
이를 만든 사람들도 알고 있었고 구현하는 방법도 엘레강스하게 해결해놨다.

예시로 비밀번호를 구현하는 방법은 이러하다.

	<input
		type="password"
        {...register("password", {
            required: true,
            pattern: {
                message:
                    "영문 + 숫자 + 특수문자를 포함하여 8자 이상으로 입력해 주세요.",
                value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,16}$/,
            },
            minLength : 8,
            maxLegnth : 16
        })}
        placeholder="영문+숫자+특수문자 8자 이상"
    />

register 함수 안에 첫번째 인자로는 데이터의 이름, 두번째 인자에 input attribute에 해당하는 조건들을
객체 형식으로 전달하면 이를 리액트에 맞게 구현해준다.
특이한 점으로는 pattern이라는 것이었는데, pattern안에 { message, value } 로 에러를 컨트롤할 정규식과
정규식의 조건에 부합하지 않을 때 사용할 에러 메시지를 넣어주면 자동으로 잡아준다!!

그렇다면 정규식에서 벗어나는 에러와 에러 메시지는 어떻게 표현할까?

	const {
		register,
		handleSubmit,
		formState: { errors },
	} = useForm({ mode: "onChange" })

useForm()에 인풋에 입력할때마다 촉발시키는 onChange모드를 전달하고
formState: { errors }를 추가로 가져오면 errors가 알아서 객체를 핸들 해준다.
일부러 에러를 일으키고 가져온 errors를 콘솔로 찍어보면 매우 재밌는 애들이 들어있다.

useEffect를 통해 errors 객체 안을 들여다 보면 에러를 일으킨 state의 이름을 key값으로
내가 input register에 지정한 정규식의 message를 에러 메시지로 가져온다.

그럼 이 메지시를 어떻게 처리하지? 우리는 매우 간단한 방법을 이미 알고 있다.

	<p className="error_message">{errors?.password?.message}</p>

에러메시지 처리까지 매우 깔끔하게 끝이 났다!

useState로 직접 구현하려면 조금 귀찮게 고생해야 할 것을 useForm()으로 구현할 경우 15줄 내외로 끝낼 수 있었다. 그리고 이는 인풋이 많이 추가된다 해도 인풋의 개수에 크게 영향을 받을 일도 없을 것이고, 이는 개발 생산성과 보다 직관적인 유지보수를 가능하게 해 줄 것이다.

지겨운 것도 새롭게 하면 재밌다!

React Hook Form을 사용하면서 사소한 단점이 있기도 했지만,
지겨운 폼을 쉽게 만들 수 있는 강력한 패키지를 찾아서 매우 만족스러웠다.

생산성이 증가했다는 지표보다는 단순반복 폼을 다루는 작업에서 벗어나
색다른 방식, 세련된 방식으로 만든다는 사실 자체도 재밌었던 것 같다.
예전부터 알고 있던 패키지라 쭉 사용해보고 싶었지만 줄곧 도입할 타이밍이 나오지 않았었다.
이번 회원고도화 덕분에 새 프로젝트에 성공적으로 적용한 것이 나 나름대로 뿌듯한 점이었다.

물론 아직 써보지 못한 API도 가득하고 기본적인 바닐라만 사용하고 있다는 느낌도 남는다.
이는 앞으로 추가로 개발할 곳에서 잔뜩 탐구해 보는 걸로 하고 ,
React Hook From 개발자들에게 찬사를 보내며 이번 첫 포스팅을 마쳐야겠다.

profile
개발+디자인을 하는 적마도사

4개의 댓글

comment-user-thumbnail
2022년 5월 10일

어마어마하네요

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

와.. 너무 정리를 잘하셨네요..!! 잘 보고 갑니다 :)

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

재밌어요!

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

알맹이 보다는 글 자체에서 많은 공감과 재미가 느껴지네요. 너무 잘 읽었습니다ㅋㅋㅋㅋㅋ폼은 지겹죠

답글 달기