📚 이론정리/React & Next.js

✏️ shadcn/ui, Zod, React-Hook-Form으로 로그인&회원가입 페이지 만들기

서카츄 2024. 10. 29. 13:35

 
 

React-hook-form 라이브러리

React-hook-form 라이브러리는 폼을 효율적으로 관리하게 하게 해주는 라이브러리다.
주로 폼 상태 관리나 유효성 검사, 폼 제출 처리에 최적화 되어있다.
기본 React 상태 관리를 사용할 때 보다 좀더 간결하게 작성할 수 있다.
함수형 컴포넌트와 hook을 사용하는 경우, 가장 사용하기 쉽고 성능적으로 좋은 폼이 react-hook-form 이다.
 
 
 
 
 

react-hook-form의 장점

이전에 사용했던 폼 유효성 검사 방식은 onChange를 만들어 setState를 해주고 바인딩 해주는 방법이었는데,
state가 변화할때마다 렌더링이 되기때문에 불필요한 렌더링이 지속적으로 일어나서 굉장히 비효율 적이었다.
또한 변화한 state를 받아서 다시 넣어주고 렌더링 하기 때문에 굉장히 느렸다.
 

 
하지만 react-hook-form 은 input의 값을 실시간으로 state에 반영하는 것이 아니라,
등록 함수가 실행될 때 한번에 처리하기 때문에 불필요한 렌더링이 제거되고,
한번에 바꿔 렌더링 하기 때문에 빠르고 효율적이다. (비제어 컴포넌트 방식)
리액트 훅 폼은 기본적으로 비제어 컴포넌트이다.
 

비제어 컴포넌트와 제어 컴포넌트
-비제어 컴포넌트
바닐라 자바스크립트 처럼 sumit함수를 실행할 때 ref 로 input값을 한번에 변경

-제어 컴포넌트
사용자의 입력을 기반으로 state를 실시간으로 관리(setState사용).
한치의 오차도 용납할 수 없는 중요한 데이터를 저장하고 있다면 제어 컴포넌트를 이용하는게 좋고,
그런게 아니라면 비제어 컴포넌트를 이용해서 성능을 높여주는게 더 좋다.

 
 
 
 
 

useForm Hook

react-hook-form에서 자주 사용하는 훅으로 폼의 상태와 메서드를 지원한다.

//예시 코드
const { register, handleSubmit, formState } = useForm({
  mode: "onBlur",
  defaultValues: { name: "", email: "" },
  resolver: zodResolver(schema),
});

 
`mode` : 언제 유효성 검사를 할지 설정한다. (onChange, onBlur, onSubmit, all 등이 있다)
`defaultValues` : 입력 필드의 초기값을 설정해준다.
`resolver` : yup이나 zod등 외부 유효성 검사 라이브러리와 통합해서 스키마 기반의 검사를 사용할 수 있다.
 
 


 

Zod

zod는 typescript의 한계 때문에 사용되는데, 스키마를 정의하여 유효성 검증을 쉽게 할 수 있다.
 
 

Docs

GitHub - colinhacks/zod: TypeScript-first schema validation with static type inference

TypeScript-first schema validation with static type inference - colinhacks/zod

github.com

 
 
 


react-hook-form & Zod 사용하기

사용할 유효성 검사 규칙을 zod schema로 정의해서 별도로 관리한다.
유효성 검사 규칙이 많아지면 schema가 길어질 수 있고, 컴포넌트 내부에 작성하면 유지보수가 어려워지기 때문,
그리고 회원가입과 로그인, 자유게시판, 등록페이지에서도 다양하게 유효성 검사가 필요하다.
이것을 한곳에서 관리하면 변경이 생길 때 훨씬 쉽게 찾고 유지보수가 가능하다.
 
예시로 회원가입 시 `영문 대&소문자, 숫자, 특수 문자 포함 8자 이상`이라면
로그인 시 동일한 규칙을 적용할 수 있다.
한 파일에 사용자 관련하여 유효성 검사를 모아 관리하면, 재사용하기 용이하기 때문이다.
 
 

예시

//users.ts

import { z } from "zod";

// extractDefaultValues 함수 구현
function extractDefaultValues<T extends z.ZodTypeAny>(
  schema: T
): Partial<z.infer<T>> {
  const parsedData: Partial<z.infer<T>> = {};

  if (schema instanceof z.ZodObject) {
    const shape = schema.shape;

    for (const key in shape) {
      if (shape[key] instanceof z.ZodString) {
        parsedData[key as keyof z.infer<T>] = "";
      }
    }
  }
  return parsedData;
}

//스키마 정의 설정
const emailSchema = z.string().email("올바른 이메일을 입력해 주세요.");

const nicknameSchema = z
  .string()
  .min(2, { message: "2글자 이상 입력해 주세요." });

const passwordSchema = z
  .string()
  .regex(
    /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).{8,}$/,
    "비밀번호는 영문, 숫자, 특수문자를 포함하여 8자 이상이어야 합니다."
  );

const passwordCheckSchema = z
  .string()
  .min(1, { message: "비밀번호를 한번 더 입력해주세요." });

//스키마 object
const signUpSchema = z
  .object({
    email: emailSchema,
    password: passwordSchema,
    passwordCheck: passwordCheckSchema,
  })
  .refine((data) => data.password === data.passwordCheck, {
    path: ["passwordCheck"],
    message: "비밀번호가 일치하지 않습니다.",
  });

const loginSchema = z.object({
  email: emailSchema,
  password: passwordSchema,
});

const myPageSchema = z.object({
  nickname: nicknameSchema,
});

//타입 지정
export type SignUpType = z.infer<typeof signUpSchema>;
export type LoginType = z.infer<typeof loginSchema>;
export type MyPageType = z.infer<typeof myPageSchema>;

//스키마 내보내기
export const userSchemas = {
  signUpSchema,
  loginSchema,
  myPageSchema,
};

//default 설정
export const userDefaultValues = {
  signUpDefaultValues: extractDefaultValues(signUpSchema),
  loginDefaultValues: extractDefaultValues(loginSchema),
  myPageDefaultValues: extractDefaultValues(myPageSchema),
};

 
user.ts 에 유효성 검사 스키마를 작성해주고,
이제 회원가입 컴포넌트에 react-hook-form과 zod를 연결해야 한다.
 
 
 
 

예시

  //SignInEmail.tsx
  
  const form = useForm<LoginType>({
    mode: "onChange",
    resolver: zodResolver(userSchemas.loginSchema),
    defaultValues: userDefaultValues.loginDefaultValues,
  });

 
`useForm` hook을 사용해서 Form 타입을 정의해주고,
`onChange`속성을 주어 입력 필드값이 변경될 때 마다 유효성 검사를 실행하도록 했다.
 
 
 
 

Input과 react-hook-form 연결

이제 react-hook-form과 input을 연동해야 유효성 검사가 시행된다.
shadcn/ui를 쓰기 때문에 shadcn/ui 의 form 컴포넌트를 공통으로 사용할 수 있는 컴포넌트를 만들어 주었다.
 

//RHFInput.tsx

"use client";

import {
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useFormContext } from "react-hook-form";

interface RHFInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  name: string;
}

export function RHFInput({ name, ...props }: RHFInputProps) {
  const { control } = useFormContext();

  return (
    <FormField
      control={control}
      name={name}
      render={({ field }) => (
        <FormItem>
          <FormControl>
            <Input
              {...props}
              {...field}
              value={field.value || ""}
              onChange={(e) => {
                field.onChange(e.target.value);
              }}
            />
          </FormControl>
          <FormMessage />
        </FormItem>
      )}
    />
  );
}

 
 
 
 
 
 
 
새로만든 RHFInput을 input 대신 사용한다.

//SignInEmail.tsx
  
return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(handleSubmit)}
        className="flex flex-col gap-3 sm:w-[275px] md:w-[375px]"
      >
        <div className="relative">
          <FaUser className="absolute top-[17px] left-5" />
          <RHFInput
            type="email"
            name="email"
            placeholder="example@example.com"
            autoComplete="email"
            autoFocus
            className="pl-11"
          />
        </div>
        <div className="relative">
          <FaLock className="absolute top-[17px] left-5" />
          <RHFInput
            type={showPassword ? "text" : "password"}
            name="password"
            placeholder="비밀번호"
            maxLength={20}
            autoComplete="new-password"
            className="pl-11"
          />
          <span
            className="absolute right-4 top-[14px] cursor-pointer"
            onClick={() => setShowPassword(!showPassword)}
          >
            {showPassword ? (
              <AiOutlineEye size={24} color="#ccc" />
            ) : (
              <AiOutlineEyeInvisible size={24} color="#ccc" />
            )}
          </span>
        </div>
        <Button
          type="submit"
          disabled={!isValid || !isDirty || isSubmitting}
          className="w-full rounded-full mt-6 p-6 transition ease-in delay-300"
        >
          {isSubmitting ? "처리 중..." : "로그인"}
        </Button>
      </form>
    </Form>
  );

 
 
그러면 내가 원하는 기능의 회원가입을 구현할 수 있다.
로그인도 똑같이 처리하면 된다 😊
 
 
 
 
 

구현 결과

 
 


 

Docs

React Hook Form - performant, flexible and extensible form library

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com