[Typescript]react-hook-form과 input custom hook 사용방법

JunSeok·2023년 8월 12일
0
post-thumbnail

많은 input들과 각각의 유효성 검사를 위한 state 관리를 좀 더 효율적으로 하고자 react-hook-form이라는 라이브러리를 사용하였다.

사용이유

  1. npm 다운로드 수가 290만(8월 12일 기준)을 넘겼고 지속적으로 업데이트를 해오고 있다.
  2. react-hook-form은 기본적으로 비제어 컴포넌트로 작동한다.
    비제어 컴포넌트를 이야기하기 전에 제어 컴포넌트에 대해 먼저 알아보자
  • React에서는 변경할 수 있는 state가 일반적으로setState()에 의해 업데이트 되고 값이 변화할 때마다 해당 Component는 re-rendering된다.
    React 내에서 모든 state data들은 immutable data이기 때문에 기존의 값을 바꾸는 변화 즉 직접 수정해서 생기는 변화에는 re-rendering하지 않는다. 이전과 같은 메모리를 가진 객체이기 때문이다.
    하여 기존의 값을 복사하여 아예 다른 객체를 만든 뒤에 값 전체를 바꾸거나 값 일부를 바꿈으로써 React에게 값이 바뀌었음을 알려주어 re-rendering을 요구하는 것이 바로 setState함수이다.
  • 이렇듯 React에 의해 내부에서 값이 제어되는 컴포넌트를 제어 컴포넌트라 부른다.

    우리는 React state를 “신뢰 가능한 단일 출처 (single source of truth)“로 만들어 두 요소를 결합할 수 있습니다. 그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.
    -출처: 리액트 공식문서

  • 제어 컴포넌트의 특징으로는 state의 값이 실시간으로 동기화되고, state의 값이 바뀔 때마다 re-rendering된다. 그렇다면 사용하는 input form element가 늘어나면 늘어날수록 컴포넌트 전체가 re-rendering되는 수가 많아져 컴포넌트 내에서 불필요한 연산이 증가할 수밖에 없다.
    더하여 관련 input에서의 유효성 검사를 위한 state값이 추가된다면 그와 관련한 코드가 길어지고 유지보수성이 확연히 떨어지게 된다.
  • 이에 반해 비제어 컴포넌트는 React에 의해 state가 제어되지 않는 컴포넌트를 말한다.
    비제어 컴포넌트는 setState()가 아닌 useRef 에 의해 업데이트되는데, 값을 참조할 때마다 같은 메모리 주소를 갖고 있기 때문에 제어 컴포넌트처럼 실시간으로 값을 동기화 시키지 않고, 불필요한 re-rendering 또한 없어진다.
    • useRef 는 heap 영역에 저장되는 일반적인 자바스크립트 객체이다.
    • 매번 rendering 때마다 동일한 객체를 제공한다. heap에 저장되어 있기 때문에 앱이 종료되지 않는 이상 같은 메모리 값을 가진다.
    • 때문에 값이 변경되어도 리렌더링이 되지 않는다. 이에 반해 setState는 매번 다른 메모리 주소를 사용하기 때문에 값이 바뀔 때마다 re-rendering이 발생한다.
    • 이러한 특징 때문에 비제어 컴포넌트가 효율적으로 보이지만, 실시간 유효성 검사와 같은 경우에는 제어 컴포넌트를 사용하는 것이 적절하다. 하지만 react-hook-form에서 유효성 검사 기능을 제공하니 적절히 잘 사용하면 비제어 컴포넌트의 장점과 제어 컴포넌트의 장점을 모두 잘 사용할 수 있다.

  1. 공식문서가 매우 잘 되어 있어, javascript와 typescript 모두 친절하게 설명해준다.

사용예시

기본적인 방법은 공식문서에 굉장히 잘 나와있지만 여기서는 input custom hook을 사용한 실제 로그인 예시를 보여주고자 한다.

login component

input custom hook에 다음과 같이 register로 한번더 감싸줘서 값을 보내주고,

<Input type="email" placeholder="example@google.com" register={{ ...register("email") }} />

input custom hook 내부

register 값을 다음과 같이 받아줬다.

// InputForm.tsx
const Input = ({ ...otherProps }) => {
    const registerInput = otherProps?.register
    return (
        <input autoComplete='off' className="w-full h-12 px-4 py-2 my-1 mb-1 transition ease-in-out bg-white border-gray-400 rounded" {...otherProps} {...registerInput} />
    )
}

export default Input

실제 사용 예시

react-hook-form을 사용함으로써 코드 양이 눈에 띄게 줄어들었다.

import { AiFillEye, AiFillEyeInvisible } from 'react-icons/ai'
import { Link, useNavigate } from 'react-router-dom'
import { SubmitHandler, useForm } from 'react-hook-form'

import Input from '../../InputForm'
import SignInGoogle from '../GoogleAuth'
import { accessToken } from '../../../store/initialState'
import { apiInstance } from '../../../apis/setting'
import { toast } from 'react-toastify'
import { useSetAtom } from 'jotai'
import { useState } from "react"


// input value type 설정
type FormData = {
    email: string,
    password: string
}

const SignInForm = () => {
    const navigate = useNavigate()
    const setToken = useSetAtom(accessToken)
    const [showPassword, setShowPassword] = useState(false)
    
    const { register, handleSubmit } = useForm<FormData>()
    // FieldValues로 입력한 email과 password 값이 담긴다.
    const clickSubmit: SubmitHandler<FormData> = async (FieldValues) => {
        const { email, password } = FieldValues
        try {
            const submitResponse = await apiInstance.post(`/user/signin`, { email, password })
            toast.success("로그인 성공!")
            navigate('/money-book/dashboard')
            setToken(submitResponse.data.accessToken)
        } catch (e: any) {
            toast.error(e.response.data.message)
        }

    }
    return (
        <section className='w-full h-full pt-[5rem]'>
            <div className="flex flex-wrap items-center justify-center w-full h-full max-w-6xl px-5 mx-auto">
                <div className="flex w-full flex-col md:w-[55%] lg:w-[40%]">
                    <h1 className="mb-8 text-4xl font-bold text-center lg:text-4xl">Sign-in</h1>
                    <form onSubmit={handleSubmit(clickSubmit)}>
                        <div className='font-semibold'>Email</div>
                        <Input type="email" placeholder="example@google.com" register={{ ...register("email") }} />
                        <div className='font-semibold'>Password</div>
                        <div className="relative">
                            <Input type={showPassword ? 'text' : "password"} placeholder="password" register={{ ...register("password") }} />
                            {showPassword ? <AiFillEyeInvisible onClick={() => setShowPassword(!showPassword)} className='absolute text-xl cursor-pointer right-3 top-5' /> : <AiFillEye onClick={() => setShowPassword(!showPassword)} className='absolute text-xl cursor-pointer right-3 top-5' />}
                        </div>
                        <div className='flex justify-between text-sm whitespace-nowrap'>
                            <p className='mb-6'>
                                Don't have a account?
                                <Link className='ml-1 text-red-600 transition ease-in-out hover:text-red-700 duration 150' to="/sign-up">
                                    Register
                                </Link>
                            </p>
                            <p className='text-blue-600 transition ease-in-out hover:text-blue-700 duration 150'>
                                <Link to="/forgot">Forgot password?</Link>
                            </p>
                        </div>
                        <button className='w-full py-3 font-semibold text-white uppercase transition bg-blue-600 rounded shadow-md px-7 hover:bg-blue-700 active:bg-blue-800 hover:shadow-lg duration 150' type='submit'>Sign in</button>
                        <div className='flex items-center my-3 before:border-t before:border-gray-300 before:flex-1 after:border-t after:border-gray-300 after:flex-1'>
                            <p className='mx-3 font-semibold text-center'>OR</p>
                        </div>
                        <SignInGoogle />
                    </form>
                </div>
            </div>
        </section>
    )
}

export default SignInForm

참고자료

공식문서
React-hook-form 왜 쓸까? 세개의 프로젝트 적용기
React: 제어 컴포넌트와 비제어 컴포넌트의 차이점

코드 보기

프론트엔드 코드 깃헙

profile
최선을 다한다는 것은 할 수 있는 한 가장 핵심을 향한다는 것

0개의 댓글