본문 바로가기
🔥React 뽀개기

react-hook-form + yup = 회원가입/로그인(feat.쿠키)

by 짱돌보리 2024. 8. 2.
728x90

✨회원가입

  • react-hook-form & yup으로 폼 제출하기

👇🏻기본 틀

 const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    trigger,
  } = useForm<FormData>({
    resolver: yupResolver(authSchema),
    mode: 'all',
  });
  
    // confirmPassword를 제외하고 폼 제출
    const onSubmit: SubmitHandler<FormData> = ({ confirmPassword, ...data }) => {
    console.log('폼 제출', data );
  };
  
   return (
    <div className="page-container">
      <div className="flex flex-col items-center justify-center">
        <Link href="/" aria-label="메인페이지로 이동">
          <Image
            src="/logo/tripzip.png"
            alt="trip.zip"
            width={300}
            height={20}
          />
        </Link>
        <form
          onSubmit={handleSubmit(onSubmit)}
          className="flex w-full max-w-640 flex-col gap-16"
        >
          <Input
            label="이메일"
            name="email"
            type="text"
            placeholder="이메일을 입력해 주세요"
            register={register('email')}
            error={errors.email}
            onBlur={() => trigger('email')}
          />
          <Input
            label="닉네임"
            name="nickname"
            type="text"
            placeholder="닉네임을 입력해 주세요"
            register={register('nickname')}
            error={errors.nickname}
            onBlur={() => trigger('nickname')}
          />
          <Input
            label="비밀번호"
            name="password"
            type="password"
            placeholder="8자 이상 입력해 주세요"
            register={register('password')}
            error={errors.password}
            onBlur={() => trigger('password')}
          />
          <Input
            label="비밀번호 확인"
            name="confirmPassword"
            type="password"
            placeholder="비밀번호를 한번 더 입력해 주세요"
            register={register('confirmPassword')}
            error={errors.confirmPassword}
            onBlur={() => trigger('confirmPassword')}
          />
          <Button
            type="submit"
            className="rounded-md"
            variant={isValid ? 'activeButton' : 'disabledButton'}
          >
            회원가입 하기
          </Button>
        </form>
        <div className="text-md mt-20 flex gap-8">
          <p>회원이신가요?</p>
          <Link
            href="login"
            className="text-custom-green-200 underline decoration-custom-green-200 underline-offset-2"
          >
            로그인하기
          </Link>
        </div>
      </div>
    </div>
  );
 const onSubmit: SubmitHandler<FormData> = ({ confirmPassword, ...data }) => {
    console.log('폼 제출', data );
  };
  • input 컴포넌트 프롭으로 register가 필수이기 때문에 어쩔 수 없이 confirmPassword에 register에 연결되었다…
  • 폼을 제출할 때 confirmPassword는 필요없어서 구조 분해 할당을 통해 confirmPassword를 제외한 나머지 필드를 data 객체로 모은다!!

[🔥React 뽀개기] - input 공통 컴포넌트로 만들기 (feat. react-hook-form + yup)

 

input 공통 컴포넌트로 만들기 (feat. react-hook-form + yup)

리액트 훅 폼+yup을 사용해 input 공통 컴포넌트 만들기✨초기작업타입에 따라 input 분기처리하기import { passwordOffIcon, passwordOnIcon } from '@/libs/utils/Icon';import Image from 'next/image';import React, { useState } from

bori-note.tistory.com

 

🐥react-query를 사용해 api post요청하기

react query type지정하는 방법 👇🏻

https://growing-jiwoo.tistory.com/118

 

[ReactQuery] useMutation과 useQuery에서 TypeScript 적용하기

❗ useMutation에 TypeScript 적용 export function useMutation< TData = unknown, TError = unknown, TVariables = void, TContext = unknown > ✔ TData: Mutate 함수가 반환하는 데이터의 타입) const { data } = useMutation(/* mutateFn */); ✔TErr

growing-jiwoo.tistory.com

 

 const mutation = useMutation<RegisterResponse, Error, RegisterRequest>(
    postUser,
    {
      onSuccess: (data: RegisterResponse) => {
        console.log('회원가입 성공', data);
        // TODO: 모달 띄우기
      },
      onError: (error: Error) => {
        console.error('회원가입 실패', error);
      },
    },
  );

  // confirmPassword를 제외하고 폼 제출
  const onSubmit: SubmitHandler<FormData> = ({ confirmPassword, ...data }) => {
    const registerData: RegisterRequest = {
      email: data.email,
      nickname: data.nickname,
      password: data.password,
    };
    console.log('폼 제출', registerData);
    mutation.mutate(registerData);
  };

💣트러블 슈팅

'(userData: RegisterRequest) => Promise<RegisterResponse>' 유형에 'UseMutationOptions<RegisterResponse, Error, RegisterRequest, unknown>' 유형과 공통적인 속성이 없습니다.ts(2559)

자꾸 postUser에 빨간 줄 뜸,,,!!!

https://beomy.github.io/tech/react/tanstack-query-v4/

 

[React] TanStack Query v4 (React Query)

TanStack Query는 비동기 작업 처리를 돕는 라이브러리입니다. v3까지는 React Query라는 이름으로 React만 지원했는데, v4 부터 React 이외의 프레임워크(Vue, Svelte, Solid)에서 사용할 수 있도록 업데이트 되

beomy.github.io

 

 const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    trigger,
  } = useForm<FormData>({
    resolver: yupResolver(authSchema),
    mode: 'all',
  });

  const mutation = useMutation<RegisterResponse, Error, RegisterRequest>({
    mutationFn: postUser,
    onSuccess: (data: RegisterResponse) => {
      console.log('회원가입 성공', data);
      // TODO: 모달 띄우기
    },
    onError: (error: Error) => {
      console.error('회원가입 실패', error);
    },
  });
  • mutationFn에다가 함수 써줘야함…

✨로그인

import tripZip from '@/../public/logo/tripZip.png';
import Button from '@/components/button';
import Input from '@/components/input/Input';
import { postLogin } from '@/libs/api/auth';
import { authSchema } from '@/libs/utils/authSchema';
import { yupResolver } from '@hookform/resolvers/yup';
import { useMutation } from '@tanstack/react-query';
import { LoginResponse } from '@trip.zip-api';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import React from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';

type FormData = {
  email: string;
  password: string;
};

export default function Signup() {
  const {
    register,
    handleSubmit,
    formState: { errors, isValid },
    trigger,
  } = useForm<FormData>({
    resolver: yupResolver(authSchema),
    mode: 'all',
  });

  const router = useRouter();

  const mutation = useMutation<LoginResponse, Error, FormData>({
    mutationFn: postLogin,
    onSuccess: (data: LoginResponse) => {
      console.log('로그인 성공', data);
      // TODO: 모달 띄우기
      router.push('/');
    },
    onError: (error: Error) => {
      if (error.message === '존재하지 않는 유저입니다.') {
        alert('존재하지 않는 유저입니다.');
        // TODO: alert 대신 모달 띄우기
      } else if (error.message === '비밀번호가 일치하지 않습니다.') {
        alert('비밀번호가 일치하지 않습니다.');
      } else {
        console.error('로그인 실패', error);
      }
    },
  });

  const onSubmit: SubmitHandler<FormData> = (data) => {
    mutation.mutate(data);
  };

  return (
    <div className="page-container">
      <div className="flex flex-col items-center justify-center">
        <Link href="/" aria-label="메인페이지로 이동">
          <Image src={tripZip} alt="trip.zip" width={300} height={20} />
        </Link>
        <form
          onSubmit={handleSubmit(onSubmit)}
          className="flex w-full max-w-640 flex-col gap-16"
        >
          <Input
            label="이메일"
            name="email"
            type="text"
            placeholder="이메일을 입력해 주세요"
            register={register('email')}
            error={errors.email}
            onBlur={() => trigger('email')}
          />
          <Input
            label="비밀번호"
            name="password"
            type="password"
            placeholder="비밀번호를 입력해 주세요"
            register={register('password')}
            error={errors.password}
            onBlur={() => trigger('password')}
          />
          <Button
            type="submit"
            className="rounded-md"
            variant={isValid ? 'activeButton' : 'disabledButton'}
          >
            로그인 하기
          </Button>
        </form>
        <div className="text-md mt-20 flex gap-8">
          <p>회원이 아니신가요?</p>
          <Link
            href="login"
            className="text-custom-green-200 underline decoration-custom-green-200 underline-offset-2"
          >
            회원가입하기
          </Link>
        </div>
      </div>
    </div>
  );
}

  const mutation = useMutation<LoginResponse, Error, FormData>({
    mutationFn: postLogin,
    onSuccess: (data: LoginResponse) => {
      console.log('로그인 성공', data);
      // TODO: 모달 띄우기
      router.push('/');
    },
    onError: (error: Error) => {
      if (error.message === '존재하지 않는 유저입니다.') {
        alert('존재하지 않는 유저입니다.');
        // TODO: alert 대신 모달 띄우기
      } else if (error.message === '비밀번호가 일치하지 않습니다.') {
        alert('비밀번호가 일치하지 않습니다.');
      } else {
        console.error('로그인 실패', error);
      }
    },
  });
  • error메세지 중에 비밀번호가 일치하지 않으면 모달 띄우기(요구사항임)

🍪로그인 시 쿠키로 정보 저장하기

쿠키 설정을 구현해서 하는 방법

document.cookie = `accessToken=${data.accessToken}; path=/; secure; samesite=strict`;
document.cookie = `refreshToken=${data.refreshToken}; path=/; secure; samesite=strict`;
  • 쿠키에 accessToken, refreshToken 저장하기
    • / 경로면 쿠키에 접근할 수 있도록
    • secure: HTTPS 연결에서만 전송되도록
    • samesite=strict: CSRF(교차 사이트 요청 위조) 공격을 방지
// 쿠키 설정 함수
export function setCookie(name: string, value: string, days: number) {
  if (typeof document !== 'undefined') {
    const expires = days
      ? `; expires=${new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString()}`
      : '';
    document.cookie = `${name}=${value}${expires}; path=/`;
  }
}

// 쿠키 삭제 함수
export function deleteCookie(name: string) {
  if (typeof document !== 'undefined') {
    document.cookie = `${name}=; Max-Age=0; path=/`;
  }
}

// 쿠키 읽기 함수
export function getCookie(name: string): string | null {
  if (typeof document !== 'undefined') {
    const nameEQ = `${name}=`;
    const cookies = document.cookie.split('; ');

    for (const cookie of cookies) {
      if (cookie.startsWith(nameEQ)) {
        return cookie.substring(nameEQ.length);
      }
    }
  }
  return null;
}

// 로그인 상태 확인 함수
export function isLogin(): boolean {
  const accessToken = getCookie('accessToken');
  return accessToken !== null;
}

 const logout = () => {
    deleteCookie('accessToken');
    deleteCookie('refreshToken');
    deleteCookie('isLogin');
    router.push('/');
  };
  • 호출해서 쓰기
  • 로그인시 쿠키에 accessToken과 refreshToken이 저장되는 것을 볼 수 있다.