프리온보딩 프론트엔드 챌린지 1월
CRUD w React Query
원티드 프리온보딩 프론트엔드 챌린지를 시작하며,
프리온보딩 프론트엔드 챌린지 사이트
프리온보딩 프론트엔드 챌린지 사전과제
사전 과제는 필수가 아니지만 효과적인 기술 역량 향상을 위해 사전 미션을 수행 후 참여하기를 권장하고 있다.
원티드 프리온보딩 프론트엔드에서 강의하는 언어와 프레임워크는 Javascript(Typescript)와 React이다.
함수 컴포넌트로 작성해야 한다.(React Hooks)
자세한 것은 사이트를 참고.
참여한 이유는 역량 향상에 있다. 개발 공부를 혼자 하면서 제일 취약했던 점은 타인의 코드를 보거나 의견을 나눌 수 없어 이렇게 사용하는 게 맞는지에 대한 확신이 없었다. 이번 챌린지는 짧게 진행되지만 해당 챌린지에 참여하면서 많은 공부가 될 것 같아 신청했다.
이번에 다루는 주제는 비동기이다. 따라서, React Query에 대해 공부하고 다음에 또 다른 주제로 챌린지가 나오면 다시 참여 신청을 하고 싶다.
아래는 사전 과제를 수행한 코드와 과정을 기록해놓았다.
요구하는 기능은 깃헙 주소로 들어가면 auth, todo 별로 확인할 수 있다.
간단한 회원가입/로그인과 todo를 구현하는 과제이다.
기능을 구현하는 데 있어서 이틀을 썼고, 하루는 회원가입/로그인을 구현하고 하루는 todo를 구현했다.
사용한 기술
react, typescript, recoil, react-query
(react-query를 사용하지 말라는 안내가 없어 사용했다.)
우선, 회원가입/로그인부터 작성해보겠다.
최소 이메일과 비밀번호를 제출해야하고 input, button을 갖도록 구성해야 한다. 또한 이메일과 비밀번호의 유효성을 확인해야 하는데 나는 여기서 정규식을 사용했다.
아직, 정규식을 자세히 알지 못해 구글링하여 해결했다.
const onChange = (event: React.FormEvent<HTMLInputElement>) => {
const {
currentTarget: { name, value },
} = event;
setUser({ ...user, [name]: value });
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// login api
const data = await userAPi(user, "login");
if (data.token) {
...
// 로그인에 성공하면 해야 할 것
} else {
// signUp api
const data = await userAPi(user);
if (data.token) {
...
// 로그인에 실패 후 회원가입으로 넘어와 성공하면 해야 할 것
} else {
// 회원가입에 실패하였다면 이미 존재하고 있는 이메일이고, 비밀번호가 틀렸다고 에러 처리한다.
setMsg("비밀번호가 틀렸습니다.");
timeOut(2000);
}
}
};
const timeOut = (time: number) => {
setTimeout(() => {
setMsg(null);
}, time);
};
...
<form onSubmit={onSubmit}>
<p>
<label htmlFor="id">email</label>
<Input
type="text"
id="id"
name="email"
onChange={onChange}
value={user.email}
/>
</p>
...
input으로 들어오는 value를 useState로 저장한다. 그리고 폼을 제출하면, api를 불러오는데, 로그인과 회원가입 api url이 엔드포인트만 제외하고 body나 method가 똑같아 변수를 통해 하나의 api로 정의했다.
(코드가 중복되어 반복되는 것이 싫어서 작성한 것인데, 이러면 나중에 오류가 발생할지, 실제로 사용하면 안되는 것인지는 모르겠다.)
api.ts
export const userAPi = async ({ email, password }: IUser, login?: string) => {
const response = await fetch(
`${process.env.REACT_APP_API_URL}/users/${login ? "login" : "create"}`,
{
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
}
);
return await response.json();
};
async, await으로 데이터가 오기 전까지 다른 코드의 작동을 일시중지하고 데이터가 도착하면 진행한다. then을 사용해도 되겠지만 async/await이 더 편한 것 같다. 이거에 대해서는 현재 js를 공부하고 있으니 나중에 공부해보자.
응답 예시로는 데이터가 성공적으로 맞게 전달되면 token
으로 회신된다. 그외의 잘못된 전달은 message
로 전달이 오는데, 모든 api가 일괄적으로 message
로 오는 것이 아니고 status
로 분리할 수 없어 에러 처리는 if/else로 해결했다.
우선, 로그인 api를 요청하여 데이터를 보내면 db
에서 이메일을 확인하고 해당 계정이 없으면 회원 가입 api로 넘어간다. 즉, 계정이 있다면, 로그인, 없다면 회원가입 후 로그인되는 루트를 탔다.
로그인 api에서 데이터가 오지 않았다면 계정이 없으므로 else
를 통해 회원가입 api를 요청한다. 여기에서도 데이터가 오지 않았다면 계정이 있다고 생각하여 비밀번호가 틀렸다고 에러를 띄운다.
=> 계정이 없었다면 생성되었을 것인데 회원가입 api에서 에러를 발생한다면 비밀번호가 틀렸다고 생각.
전역 상태 관리 툴인 recoil
을 사용하여 에러 메세지를 관리해준다. 에러 메세지의 컴포넌트는 동일하기 때문에 전역 변수로 빼주어 어디에서든 사용할 수 있도록 하였다. 또한, 에러 메세지를 상단에 띄운 후 2초가 지나면 없어지도록 setTimeout
을 사용하였다.
const emailRegEx =
/^[A-Za-z0-9]([-_.]?[A-Za-z0-9])*@[A-Za-z0-9]([-_.]?[A-Za-z0-9])*\.[A-Za-z]{2,3}$/;
const passwordRegEx = /^[A-Za-z0-9]{8,20}$/;
const emailMatch = (email: string) => {
return emailRegEx.test(email);
};
const passwordMatch = (password: string) => {
return passwordRegEx.test(password);
};
<input
type="submit"
value="Login / Sing up"
id="button"
disabled={
passwordMatch(user.password) && emailMatch(user.email)
? false
: true
}
/>
로그인을 시도할 때 유효성 검사를 실시한다.
이메일에는 @
,.
을 포함해야하고 비밀번호는 8자
이상 입력해야한다.
그렇지 않으면 버튼을 비활성화하고 조건을 만족해야 버튼이 활성화 되도록 하는 게 조건이다.
emailRegEx
, passwordRegEx
에 이메일 정규식을 할당한다.
이메일에 @
,.com
을 포함해야하고, 비밀번호는 8자리가 넘어야 한다.
함수를 사용해 button의 disabled를 관리한다. 처음에는 상태 관리로 state를 사용하려고 했으나 그러기 위해서는 해야할 것을 생각하니 비효율적이라는 생각이 들었다.
(또 if 사용하고... 기타 등등)
test
라는 메서드를 이메일, 비밀번호 둘 다 사용해준다. test
는 조건에 맞으면 true/false
로 반환해주는 데 이걸 사용해서 disabled를 관리하면 되겠다 싶었다. 작동은 생각했던 대로 움직여서 그대로 놔두었다.
const setLoginInit = ({ token }: IData) => {
seIstLogged(true);
localStorage.setItem("token", token);
navigator("/");
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// login api
const data = await userAPi(user, "login");
if (data.token) {
setLoginInit(data);
} else {
// signUp api
const data = await userAPi(user);
if (data.token) {
setLoginInit(data);
} else {
setMsg("비밀번호가 틀렸습니다.");
timeOut(2000);
}
}
};
로그인 혹은 회원가입에 성공하여 token
을 얻었다면 이것을 로컬 스토리지에 저장한다. 다음 번에 로그인 시 토큰이 존재한다면 루트 경로로 리다이렉트 시키는 것이 조건인데, 나는 조금 다르게 하였다.
app.tsx
const [isLoggedIn, setIsLoggedIn] = useRecoilState(isLogged);
useEffect(() => {
if (localStorage.getItem("token")) {
setIsLoggedIn(true);
}
}, [isLoggedIn]);
home.tsx
const isLoggedIn = useRecoilValue(isLogged);
...
{isLoggedIn ? (
<ToDosBox>
<GetTodos />
<CreateTodoList />
</ToDosBox>
) : (
<p>로그인 해주세요</p>
)}
...
로그인의 상태를 변수로 할당하여 전역 관리해준다. 사이트를 클릭하면 먼저 app으로 들어오기 때문에, app.tsx에서 token
이 로컬 스토리지에 존재하는지 확인한다.
존재한다면 로그인을 했다고 생각하여 true
를 반환해준다. 그리고 로그인이라는 버튼은 렌더링하지 않고 로그아웃 버튼과 todo list를 렌더링 한다.
이미 token
이 있다면 로그인 페이지에 들릴 필요가 없을 것 같다고 생각했다. 그래서 리다이렉트 시켜주기 보단 바로 app에서 로그인 시켜주었다.
(원래 요구했던 조건이 아니어서 실제로 이렇게 하면 안되겠지만 공부하는 입장이기에 좀더 효율적이라고 판단한 것으로 변경하였다.)