티스토리 뷰

⭐ React-hook-form

React Hook Form은 React에서 간단하고 효율적으로 폼을 관리하기 위한 라이브러리입니다.

 

폼 관리 방식

React Hook Form은 Uncontrolled 방식과 Controlled 방식이 있다.

  1. Uncontrolled : register 함수를 사용하고 두 번째 파라미터를 사용하여 유효성 검증이 가능하며 DOM과 직접적으로 연결되어 성능이 우수하다. 
  2. Controller : Controller 컴포넌트의 rules 속성을 사용하여 유효성 검증이 가능하며 주로 외부 UI 라이브러리와 통합 시 사용한다.

API : useForm Hook

  • register : Uncontrolled 방식으로 사용하기 위해 사용됩니다. 요소의 속성으로 전달되어야 하는 값들을 반환하는 함수입니다.
<input {...register('username', { required: true })} />
  • control : Controlled 방식으로 사용하기 위해 사용됩니다. UI 라이브러리와 함께 사용할 때 자주 사용됩니다.
<Controller
  name="username"
  control={control}
  rules={{ required: "Username is required" }}
  render={({ field }) => <TextField {...field} />}
/>

handleSubmit : 폼 제출 시 호출되는 함수로 유효성 검증을 통과한 데이터를 콜백으로 전달한다.

<form onSubmit={handleSubmit(onSubmit)}>
  ...
</form>

watch : 사용자 입력 값의 변화를 감시하기 위한 함수입니다.

 

formState : 폼의 상태를 저장하는 객체로, 대표적으로 errors 객체를 통해 유효성 에러 정보를 가져올 수 있습니다.

const { errors } = formState;

Controller 컴포넌트

Controller 컴포넌트를 사용하면 UI 라이브러리와 React Hook Form을 함께 사용할 수 있습니다.

  • name : 요소를 구분하기 위한 값으로 같은 폼으로 묶여 있는 요소끼리 유니크해야 합니다.
  • control : useForm 훅의 반환 값인 control을 그대로 전달하면 됩니다.
  • render : 화면에 렌더링할 요소를 반환하는 함수입니다.
  • defaultValue : 컴포넌트 수준에서 필드 초기값을 설정합니다.
  • rules : 유효값 검증 규칙 설정합니다.
<Controller
  name="email"
  control={control}
  defaultValue=""
  rules={{ required: "Email is required" }}
  render={({ field }) => <TextField {...field} />}
/>

Controller 활용하기

 

저는 React Hook Form의 Controller와 공통 컴포넌트를 활용하여 반복되는 필드 작성과 유효성 검증 로직 관리를 해결했습니다.

 

 

TextFieldItem 컴포넌트

  • Controller를 사용하여 유효성 검증과 외부 라이브러리 MUI 통합
  • props로 itemKey, title, requiredText, control, errors를 받아 동적으로 동작
  • sx 스타일링을 통해 에러 메시지를 애니메이션 효과로 표시하여 사용자 경험을 향상
const TextFieldItem = memo(({ itemKey, title, requiredText, control, errors }) => {
  return (
    <Box>
      <Typography fontWeight="bold" sx={{ mb: 1 }}>
        {title}
      </Typography>
      <Controller
        name={itemKey}
        control={control}
        defaultValue=""
        rules={{ required: requiredText }}
        render={({ field }) => (
          <>
            <TextField
              {...field}
              fullWidth
              variant="outlined"
              placeholder={requiredText}
              error={!!errors}
            />
            <FormHelperText
              sx={{
                transition: "all 0.4s ease-in-out",
                opacity: errors ? 1 : 0,
                visibility: errors ? "visible" : "hidden",
                color: theme.palette.error.main,
              }}
            >
              {errors?.message}
            </FormHelperText>
          </>
        )}
      />
    </Box>
  );
});

 

useForm 활용

  • handleSubmit, control, formState.errors를 사용하여 폼 데이터를 검증 및 관리
  • onBlur 모드 설정으로 블러 이벤트에서 유효성 검증 실행
const { handleSubmit, control, formState: { errors } } = useForm({ mode: "onBlur" });

const handleLogin = useCallback((data) => {
  console.log("Form Data:", data);
  navigator("/");
}, [navigator]);

사용 사례

  • TextFieldItem 컴포넌트를 활용하여 간결하고 직관적인 폼 필드를 구현
  • 아래와 같이 다양한 필드를 공통 컴포넌트로 재사용 가능
<TextFieldItem
  itemKey="id"
  title="ID"
  requiredText="아이디를 입력해주세요"
  control={control}
  errors={errors.id}
/>

<TextFieldItem
  itemKey="password"
  title="Password"
  requiredText="비밀번호를 입력해주세요"
  control={control}
  errors={errors.password}
/>

 


⭐ 공통 컴포넌트에서 Action 재사용성 높이기 

React Hook Form을 사용하면서 SplitLayout이라는 공통 컴포넌트를 설계했습니다.

이 컴포넌트는 입력 영역을 props로 받아오며, 항상 버튼을 포함하는 구조를 가지고 있습니다.

특징적인 요구사항은 버튼을 클릭했을 때, 폼을 제출(submit)하는 동작을 수행해야 한다는 점이었습니다.

초기

SplitLayout 자체를 <form onSubmit={handleSubmit(handleLogin)}>로 감싸는 구조를 사용했습니다.
이 방식은 다음과 같은 문제점을 드러냈습니다:

  1. 가독성 저하: <form> 내부에 너무 많은 불필요한 태그가 포함되니 유지보수와 가독성이 떨어짐
  2. 재사용성 감소: SplitLayout이 특정 로직에 종속되어 다른 폼에서 재사용하기 어려움

개선

  1. 폼 제출과 관련된 로직을 SplitLayout 내부로 이동.
  2. onSubmit을 props로 전달받아 폼 제출 시 동작을 유연하게 처리
const Login = () => {
  const navigator = useNavigate();

  const {
    handleSubmit,
    control,
    formState: { errors },
  } = useForm({ mode: "onBlur" });

  const handleLogin = useCallback(
    (data) => {
      console.log("data", data);
      navigator("/");
    },
    [navigator]
  );

  return (
    <SplitLayout
      title="Log in"
      buttonTitle="Log in"
      buttonAction={handleSubmit(handleLogin)}
    >
      <Box
        display="flex"
        flexDirection="column"
        justifyContent="space-between"
        flex="1"
        gap={10}
      >
        <Stack spacing={6}>
          <TextFieldItem
            itemKey="id"
            title="ID"
            requiredText="아이디를 입력해주세요"
            control={control}
            errors={errors.id}
          />
          <TextFieldItem
            itemKey="pw"
            title="PW"
            requiredText="비밀번호를 입력해주세요"
            control={control}
            errors={errors.pw}
          />
        </Stack>
      </Box>
    </SplitLayout>
  );
};
const SplitLayout = ({ title, children, buttonTitle, buttonAction }) => {
  return (
    <Grid container sx={{ height: "100vh", overflow: "auto" }}>
      <Grid size={6}>
      	// 생략
        <form onSubmit={buttonAction}>
          <Box padding={4}>{children}</Box>
          <Box display="flex" justifyContent="center">
            <Button
              variant="contained"
              type="submit"
              sx={{
                width: "304px",
                height: "40px",
                margin: "auto",
                borderRadius: "40px",
                backgroundColor: "hyundai.default",
              }}
            >
              {buttonTitle}
            </Button>
          </Box>
        </form>
      </Grid>
    </Grid>
  );
};

 

장점

  1. 가독성 향상: 폼 관련 로직이 SplitLayout에 캡슐화되어 관리가 쉬워짐.
  2. 재사용성 증가: 버튼과 폼 레이아웃을 관리하는 SplitLayout은 다양한 폼에서 재사용 가능.
  3. 유연성 확보: onSubmit을 외부에서 전달받아 다양한 동작을 쉽게 구현.

결론

SplitLayout 내부에서 폼 로직을 처리하며, 자식 컴포넌트는 단순히 입력 필드와 폼 동작을 전달하는 역할만 수행합니다.
결과적으로 컴포넌트의 가독성과 재사용성을 모두 높일 수 있는 깔끔한 구조를 제공하게 됩니다.

 

 

 

참고

https://beomy.github.io/tech/react/react-hook-form/

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함