회원가입
회원가입 / 로그인 기능은 대부분의 웹사이트에서 제공하는 기능입니다.
회원가입을 하는 로직을 작성하기 위해 필요한 것은 다음과 같이 나열할 수 있습니다.
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>;
인풋이 세개밖에 없는 간단한 조건이었기에 쉽게 작성할 수 있었습니다.
비밀번호를 설정하는 정규식을 작성하는게 조금 귀찮았는데
영어와 숫자만 입력받을 수 있는 형태로 정규식을 작성했습니다.
정규식의 출처는 아래 블로그입니다.
[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 버튼을 누르게되면
에러메시지가 표시되게됩니다.
마치며
라이브러리를 통해 귀찮은 작업들을 깔끔하게 관리할 수 있는 것이 좋은 것 같습니다.
공부할 때에는 조금 난해했지만 막상 잘 조합해 사용하고보니 코드가 마음에드네요
'cheetsheet' 카테고리의 다른 글
자바스크립트로 구현하는 디바운싱 예제(trailing edge) (0) | 2023.04.22 |
---|---|
node-sass 설치 에러 해결 방법과 원인.. (0) | 2023.04.16 |
length 프로퍼티 없이 이터레이터로 Each 구현하기 (0) | 2023.03.17 |
콜백으로 준식 지옥을 만들어보자 (8) | 2023.03.16 |
reset.css 인풋,버튼 태그의 배경 바꾸기와 placeholder 글자 색 바꾸기 (0) | 2023.03.04 |