cheetsheet

리액트로 회원가입 로직 만들어보기

냠냠맨 2023. 6. 22. 19:29

회원가입

회원가입 / 로그인 기능은 대부분의 웹사이트에서 제공하는 기능입니다.

회원가입을 하는 로직을 작성하기 위해 필요한 것은 다음과 같이 나열할 수 있습니다.

 

1. post 요청을 보내야하니 일반적인 경우 cors 설정이 필요합니다.

2. 적절한 아이디, 이메일, 비밀번호를 입력받아야하므로 validation이 필요합니다.

3. 여러개의 input을 관리하며 모든 입력이 마쳐졌을 때 요청을 보내야합니다.

4. 작은 단위로 나뉜 컴포넌트 및 input들이 하나의 form에 종속되어야합니다.

5. 회원가입이 완료되면 적절히 리다이렉팅해주어야합니다(로그인페이지로 리다이렉팅 등)

 

사용하는 개발 환경은 다음과 같았습니다.

react / typescript / react-query / zod / react-hook-form / msw

 

submit이 완료되어 서버에 요청을 보낼때는 react-query를 이용할것이며

밸리데이션 및 관리는 zod와 react-hook-form으로 진행할 것입니다.


먼저 msw로 mocking server 띄우기

https://xionwcfm.tistory.com/342

 

msw를 이용해 데이터 모킹하고 Suspense와 함께 react-query로 가져오기

해본이유 next.js의 app dir을 이용하는 프로젝트에서 json파일을 통해 mock data로 fetching을 시도해보던 중 페칭 자체는 잘 되었지만 에러가 발생하는 것을 확인했습니다. 그러다가 msw를 통해서 한번

xionwcfm.tistory.com

위 포스트를 참고해주세요

 

handlers.ts에는 endpoint와 요청종류에 따른 응답을 정의해주겠습니다.

handlers.ts

export const handlers = [
  rest.post('/users/signup', async (req, res, ctx) => {
    const newUser = await req.json();
    const validation = users.findIndex((user) => user.email === newUser.email);

    if (validation !== -1) {
      return res(ctx.status(400), ctx.json('이미 가입한 이메일입니다.'));
    } else {
      newUser.userid = Number(new Date());
      users.push(newUser);
      return res(ctx.status(201), ctx.json('signup successful'));
    }
  }),
  rest.post('/users/login', async (req, res, ctx) => {
    const accessToken = 'dummy-access-token';

    const loginUser = await req.json();
    const validation = users.findIndex(
      (user) => user.email === loginUser.email && user.password === loginUser.password
    );

    if (validation === -1) {
      return res(ctx.status(400), ctx.json('login failed'));
    } else {
      return res(
        ctx.status(200),
        ctx.json('login successful'),
        ctx.set('authorization', `Bearer ${accessToken}`)
      );
    }
  }),
  rest.get('/users/logout', (_, res, ctx) => {
    return res(ctx.status(200), ctx.json('logout successful'));
  })
  ]

users/signup

users/login

users/logout

으로 엔드포인트들을 정의해주었습니다.

간단하게 작업하는 것이니 빡빡하게 로직을 작성하진 않았습니다.

        ctx.set('authorization', `Bearer ${accessToken}`)

의 형태로 응답헤더에 key, value를 설정해줄 수 있다는 것은 기억하면 좋을듯합니다.

그외에 msw를 위해 해주어야하는 작업들이 궁금하다면 위 포스트를 참고해주세요


리액트 쿼리로 mutation 작성하기

useSignupMutation.tsx

import { SignupType } from '@/pages/SignupPage';
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';

const url = '';

const getHeader = (response: Response, get: string) => {
  return response.headers.get(get);
};

const postSignup = async (signupData: SignupType) => {
  const response = await fetch(`${url}/users/signup`, {
    method: 'POST',
    body: JSON.stringify(signupData),
  });

  // const header = getHeader(response, 'authorization');
  const result = await response.json();
  return result;
};

const useSignupMutation = () => {
  const navigate = useNavigate();
  const signupMutation = useMutation({
    mutationFn: (newSignupData: SignupType) => postSignup(newSignupData),
    onSuccess(data, variables, context) {
      navigate('/users/login');
    },
    onSettled: () => {},
    onError: () => {},
  });
  return signupMutation;
};

export default useSignupMutation;

커스텀훅으로 작성하여 로직을 분리해줄 생각입니다.

const getHeader = (response: Response, get: string) => {
  return response.headers.get(get);
};

fetch 요청에 대한 응답의 타입을 적어주고싶으면

Response 타입을 이용할 수 있습니다.

response.headers.get('얻고자하는 헤더의 key 값')의 형태로

헤더에 담긴 밸류를 가져올 수 있습니다.


const postSignup = async (signupData: SignupType) => {
  const response = await fetch(`${url}/users/signup`, {
    method: 'POST',
    body: JSON.stringify(signupData),
    credentials: 'include',
    headers: {
      Origin: 'http://localhost:5173',
      'Access-Control-Request-Method': 'POST',
    },
  });

다음은 useMutation 함수의 mutationFn key에 들어갈 함수를 정의해주겠습니다.

fetch 함수로 get 요청 이외의 요청을 보내고자 할 때에는

fetch 함수의 두번째 인자로 전달하는 객체에 method를 정의해주면 됩니다.

https://xionwcfm.tistory.com/235

 

CORS / SOP가 머임

🐕 SOP (Same - origin - policy) 동일 출처 정책 동일 출처 정책은 웹 애플리케이션의 중요한 보안 모델입니다. 동일 출처 정책은 같은 출처(Origin)의 리소스만 공유가 가능하다는 정책인데 인간이 보기

xionwcfm.tistory.com

fetch 함수에 전달한 옵션들이 자세하게 궁금하다면 위 링크를 참고해주세요

const useSignupMutation = () => {
  const navigate = useNavigate();
  const signupMutation = useMutation({
    mutationFn: (newSignupData: SignupType) => postSignup(newSignupData),
    onSuccess(data, variables, context) {
      navigate('/users/login');
    },
    onSettled: () => {},
    onError: () => {},
  });
  return signupMutation;
};

이부분은 react-router-dom과 react-query를 사용하는 커스텀훅 로직입니다.

useNavigate() 함수를 통해 리다이렉팅을 쉽게 구현할 수 있습니다.

 

react-query가 제공하는 useMutation 함수는

mutationFn 이외에도 여러가지 옵션 키를 전달해줄 수 있습니다.

이번에 고려해본 것은 onSuccess 함수였는데

이름에서 예상할 수 있듯이

요청이 성공했을때 트리거되는 함수입니다. 

 

이 외에도 성공/ 실패 여부에 상관없이 실행되는 onSettled 함수와

onError 함수가 있습니다. 상황에 맞게 사용할 수 있지만

이번에는 회원가입에 성공하였을때 login 화면으로 리다이렉팅시켜주기위해

onSuccess에 navigate를 걸어주었습니다.

 

참고로 react-router-dom이 제공하는 useNavigate함수 역시 훅이기때문에

리액트 컴포넌트에서만 사용가능하다는 점 유의하시기 바랍니다.


zod로 validation 하기

const passwordRegex = new RegExp(/^(?=.*[a-zA-Z])(?=.*[0-9])/);

const signupSchema = z.object({
  nickname: z.string().min(2).max(10),
  email: z
    .string()
    .min(1, { message: 'This field has to be filled' })
    .email({ message: 'Invalid email address' }),
  password: z.string().min(8).regex(passwordRegex),
});

export type SignupType = z.infer<typeof signupSchema>;

인풋이 세개밖에 없는 간단한 조건이었기에 쉽게 작성할 수 있었습니다.

비밀번호를 설정하는 정규식을 작성하는게 조금 귀찮았는데

영어와 숫자만 입력받을 수 있는 형태로 정규식을 작성했습니다.

정규식의 출처는 아래 블로그입니다.

https://velog.io/@bunny/%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%A0%95%EA%B7%9C%EC%8B%9D-%ED%8C%A8%ED%84%B4

 

[Javascript ]비밀번호 정규식 패턴

영문 숫자 조합 8자리 이상 영문 숫자 특수기호 조합 8자리 이상

velog.io

다만 저는 min / max를 zod가 제공하는 메서드를 이용해 처리하기 위하여

글자수제한을 거는 정규식을 제거한 형태로 작성했습니다.

const phoneRegex = new RegExp(/^01([0|1|6|7|8|9])-?([0-9]{3,4})-?([0-9]{4})$/);

핸드폰 번호를 입력받는 정규식은 다음 형태로 사용하는 편입니다.

zod에 관해 더 자세한 정보가 필요하신 경우

https://xionwcfm.tistory.com/346

 

zod 라이브러리 사용법 간단히 익히기

zod zod는 타입스크립트를 우선하는 스키마 선언 / 검증 라이브러리입니다. validation을 도와준다고도 생각할 수 있는데 문법은 간결하고 쉬우면서도 기능은 강력합니다. 공식 documentation에 따르면

xionwcfm.tistory.com

https://xionwcfm.tistory.com/347

 

zod를 이용해 회원가입 폼을 만들어보기

zod는 타입스크립트를 우선하는 스키마 선언 / 검증 라이브러리입니다. 타입스크립트의 타입체킹을 보완해주는 역할을 수행한다고도 볼 수 있는데 쉽게 생각하면 폼,인풋에 필연적으로 따라오

xionwcfm.tistory.com

위 포스트들을 참고해주세요


react-hook-form의 useForm

 

form의 상태를 관리하는 것은 어렵지는 않지만

코드가 지저분해지기 굉장히 쉽습니다.

react-hook-form은 그러한 문제를 쉽게 해결시켜주는 라이브러리입니다.

그뿐만 아니라 폼에 필연적으로 따라오게되는 리렌더링 문제 역시 최적화가 잘되어있어

여러모로 장점이 많은 라이브러리라고 생각이 듭니다.

 

이번에는 zod와 통합하여 사용하기 위해

@hookform/resolvers에서 제공해주는 zodResolver 함수를 이용할 것입니다.

  const signupMutation = useSignupMutation();
  const signup = useForm<SignupType>({ resolver: zodResolver(signupSchema) });
  const onSubmit: SubmitHandler<SignupType> = async (data) => {
    const mutate = await signupMutation.mutateAsync(data);
  };

서버에 post를 보내는 것은 react-query로 작성한 커스텀훅이 처리하고

validation은 zod로 처리하고 나니 컴포넌트 영역에서 작성할 로직이 많이 간결해진것을 알 수 있습니다.

재미있는 점은 onSubmit 함수를 타이핑하는 방법입니다.

SubmitHandler라는 타입을 통해 쉽게 타이핑할 수 있다는 것입니다.

이렇게 타이핑을 해주고 나면 

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

이런 형태로 간결하게 작성할 수 있습니다.

그런데 약간의 문제가 있었습니다.

 

협업을 통해 프로젝트를 진행하고 있었는데

로그인 페이지의 마크업을 맡은 인원이 제가 아니었기때문에

컴포넌트형태로 있어 register 함수의 호출값을 스프레드하는것에 제한사항이 있었습니다.

                <LabelInput
                  title={'Display Name'}
                  isWithLink={false}
                  {...signup.register('nickname')}
                />
                <LabelInput title={'Email'} isWithLink={false} {...signup.register('email')} />
                <LabelInput
                  title={'Password'}
                  isWithLink={false}
                  type={'password'}
                  {...signup.register('password')}
                />

따라서 타인이 작성한 LabelInput 컴포넌트의 props와 관련하여

타이핑을 해줄 필요가 있었습니다.

import { ComponentPropsWithRef, forwardRef, Ref } from 'react';

interface LabelInputProps extends ComponentPropsWithRef<'input'> {
  type?: string;
  isWithLink: boolean;
  title: string;
  linkText?: string;
}

const LabelInput = (
  { type, isWithLink, title, linkText, ...attributes }: LabelInputProps,
  ref: Ref<HTMLInputElement>
)


export default forwardRef(LabelInput);

리액트에서 제공해주는 ComponentPropsWithRef<> 타입을 이용하여

타이핑을 해주었습니다.

ref를 받을 수 있도록 코드를 작성해주고 싶었는데 ref를 타이핑하는 것은

위와 같이 리액트가 제공하는 Ref 타입에 제네릭을 전달해주면 됩니다.

forwardRef를 통해 감싸주어야 ref를 정상적으로 자식컴포넌트에서 사용할 수 있으니

이점 또한 유의하시기 바랍니다.

              {(signup.formState.errors.email ||
                signup.formState.errors.password ||
                signup.formState.errors.nickname) && (
                <span className="  text-xs text-red-500">
                  something went wrong please check your password, email, name, daham
                </span>
              )}

에러메시지 처리같은 경우에는

공통된 형태로 보여줄 생각으로 || 연산자를 이용해 작성해주었습니다.

이렇게 양식에 맞지 않게 작성된 상태로 sign up 버튼을 누르게되면

에러메시지가 표시되게됩니다.


마치며

라이브러리를 통해 귀찮은 작업들을 깔끔하게 관리할 수 있는 것이 좋은 것 같습니다.

공부할 때에는 조금 난해했지만 막상 잘 조합해 사용하고보니 코드가 마음에드네요

반응형