프론트엔드 온보딩에 참여하게 되었는데, 사전과제로 TodoList 만들기를 했었다. 공고를 늦게 발견하여 시간에 쫓겨 황급히 만들었는데..
그러다 보니 더럽고 난잡하게 만들어지고 필요 구현 사항들도 완전히 만들지 못하게 되었다.
그렇게 완전하지 못한 사전과제를 기반으로 수업을 들었는데... 멘토님의 지적사항 하나하나가 뜨끔하게 만들었고 코드를 잘 짜는 방법을 배울 수 있게 되어서 좋았다. 아직 실무 경험이 없어 몰랐던 것들을 배울 수 있을 것으로 기대된다. 나는 내용과 적용하는 게 어렵게 느껴졌지만 시간이 오래 걸리더라도 꼭 Todolist를 Best Practice로 만들기 위해 차근차근 리펙토링 해볼 예정이다.
1. Token과 Authorication의 상수 선언
- 회원가입을 통해 발급 받을 수 있는 토큰을 기존에는 "access-token"이라는 string으로 사용했었다. 반면에 같이 수강을 듣던 수강생의 좋은 코드 예시에서 토큰을 상수로 선언하여 토큰 상수가 필요한 데를 받아서 쓰는 형태로 사용하는 좋은 예시를 배울 수 있었다. 이렇게 상수로 만들어 놓고 사용한다면 자동완성으로 오타를 방지할 수 있는 장점이 있고 유지 보수에도 이점이 있을 거라 판단돼서 나 또한 해당 방법을 적용하기로 했다.
- 새로 생성된 코드
export const ACCESS_TOKEN_KEY = "access-token" as const;
export const REQUEST_KEY = "Authorization" as const;
그리고 해당 내용을 설명하면서 인증과 인가에 대해 추가 설명을 해주셨다.
- 인증(Authentication) : "user가" 맞아? 확인하는 것", 회원가입과 로그인과 같이 사용자가 맞는지를 확인하는 절차
- 인가(Authorization) : "이걸 사용할 수 있는 권한이 있는거 맞아?를 확인하는 것 ", 유저가 요청하는 reuqest를 실행할 수 있는 권한이 있는 유저인가를 확인하는 절차이다. 예를 들면 해당 유저는 고객 정보를 볼 수 있지만 수정할 수는 없는 경우가 있다.
2. 관심사의 분리(Separation of Concerns, SoC)
코드의 유지보수성과 가독성을 높이기 위해선 복잡하게 얽혀있는 로직들을 구분하여 분리하는 게 중요하다. 이를 위해 코드를 주제별로 즉 관심사 별로 나누는 것을 관심사의 분리라고 한다.
그리고 인증 / 인가와 같은 앱 곳곳에서 쓰이게 되는 특징을 가지는것과 같이 단일한 기능이 여러 지점에 걸쳐 나타날 때 이를 횡단 관심사(Cross Cutting Concern) 라고 부른다.
따라서 횡단 관심사들을 별도의 레이어로 분리해서 보다 일관되게 처리할 수 있는 방법이 있다면 더 바람직한 설계가 될 수 있다.
또한 로직과 뷰를 분리하여 커스텀훅으로 컴포넌트를 구성하는 방식을 채택하였다.
ex)
- api 폴더를 생성하여 api 통신로직을 분리
- login/signup view 부분과 Logic 부분을 분리
- 분리 전
import React, { useState, Dispatch, SetStateAction } from "react";
import { useNavigate } from "react-router-dom";
import classes from "./LoginForm.module.css";
interface loginFromProps {
renderLogin: Dispatch<SetStateAction<boolean>>;
}
const LoginForm = (props: loginFromProps) => {
const [enteredEmail, setEnteredEmail] = useState<string>("");
const [enteredPassword, setEnteredPassword] = useState<string>("");
const navigate = useNavigate();
const [emailMessage, setEmailMessage] = useState<string>("");
const [passwordMessage, setPasswordMessage] = useState<string>("");
const [emailValidation, setEmailValidation] = useState<boolean>(false);
const [passwordValidation, setPasswordValidation] = useState<boolean>(false);
const emailChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const emailRegex =
/([\w-.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/;
const emailCurrent = event.target.value;
setEnteredEmail(emailCurrent);
if (!emailRegex.test(emailCurrent)) {
setEmailMessage("올바른 이메일 형식을 입력해주세요!");
setEmailValidation(false);
} else {
setEmailMessage("올바른 이메일 형식입니다");
setEmailValidation(true);
}
};
const passwordChangeHandler = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const passwordRegex =
/^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,25}$/;
const passwordCurrent = event.target.value;
setEnteredPassword(passwordCurrent);
if (!passwordRegex.test(passwordCurrent)) {
setPasswordMessage(
"숫자 + 영문 + 특수문자 조합으로 8자리 이상 입력해주세요!"
);
setPasswordValidation(false);
} else {
setPasswordMessage("올바른 패스워드 형식입니다!");
setPasswordValidation(true);
}
};
const onLoginHandler = (event: React.MouseEvent) => {
event.preventDefault();
const data = {
email: enteredEmail,
password: enteredPassword,
};
fetch("http://localhost:8080/users/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((res) => {
if (res.message === "성공적으로 로그인 했습니다") {
localStorage.setItem("access-token", res.token);
navigate("/todo");
}
});
};
const moveSignUpHandler = (event: React.MouseEvent) => {
event.preventDefault();
props.renderLogin(false);
};
return (
<>
<form className={classes["form-login"]}>
<h1>로그인</h1>
<label htmlFor="loginemail">이메일</label>
{enteredEmail.length > 0 && (
<span
className={`${classes["email-message"]} ${
emailValidation ? classes.success : classes.error
}`}
>
{emailMessage}
</span>
)}
<input id="loginemail" type="email" onChange={emailChangeHandler} />
<label htmlFor="loginpassword">패스워드</label>
{enteredPassword.length > 0 && (
<span
className={`${classes["password-message"]} ${
passwordValidation ? classes.success : classes.error
}`}
>
{passwordMessage}
</span>
)}
<input
id="loginpassword"
type="password"
onChange={passwordChangeHandler}
/>
<button
onClick={onLoginHandler}
disabled={!(emailValidation && passwordValidation)}
className={
!(emailValidation && passwordValidation)
? classes["button-disabled"]
: classes["button-abled"]
}
>
로그인
</button>
<button className={classes["button-abled"]} onClick={moveSignUpHandler}>
회원가입
</button>
</form>
</>
);
};
export default LoginForm;
분리 후
import useAuthChangeHanler from "../../hook/auth/useAuthChangeHanler";
import useLoginSubmitHandler from "../../hook/auth/useLoginSubmit";
import useNavigatePageHanlder from "../../hook/useNavigePageHanlder";
import classes from "./LoginForm.module.css";
const LoginForm = () => {
const { emailMessage, passwordMessage, emailValidation, passwordValidation, email, password, emailChangeHandler, passwordChangeHandler} = useAuthChangeHanler();
const { onLoginSubmit } = useLoginSubmitHandler();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log({ email, password });
onLoginSubmit({ email, password });
};
const { navigateSignUpHandler } = useNavigatePageHanlder();
return (
<>
<form className={classes["form-login"]} onSubmit={handleSubmit}>
<h1>로그인</h1>
<label htmlFor="loginemail">이메일</label>
{email.length > 0 && (
<span
className={`${classes["email-message"]} ${
emailValidation ? classes.success : classes.error
}`}
>
{emailMessage}
</span>
)}
<input id="loginemail" type="email" onChange={emailChangeHandler} />
<label htmlFor="loginpassword">패스워드</label>
{password.length > 0 && (
<span
className={`${classes["password-message"]} ${
passwordValidation ? classes.success : classes.error
}`}
>
{passwordMessage}
</span>
)}
<input
id="loginpassword"
type="password"
onChange={passwordChangeHandler}
/>
<button
disabled={!(emailValidation && passwordValidation)}
className={
!(emailValidation && passwordValidation)
? classes["button-disabled"]
: classes["button-abled"]
}
>
로그인
</button>
<button
className={classes["button-abled"]}
onClick={navigateSignUpHandler}
>
회원가입
</button>
</form>
</>
);
};
export default LoginForm;
(아직 길지만 확연히 줄어든 코드 좀 더 줄여보잣!)
3. fetch를 axios로 변환
Fetch와 axios는 모두 promise 기반의 HTTP 클라이언트입니다. 즉 이 클라이언트를 이용해 네트워크 요청을 하면 이행(resolve) 혹은 거부(reject)할 수 있는 promise가 반환된다.
axios로 바꾸는 이유: 코드가 양을 줄일 수 있고 가독성이 높아진다. axios intercepter 라이브러리를 사용하여 동일한 content-type , token와 같은 헤더값 처리를 효율적으로 하기 위하여
4. react-auery 적용
react-query와 mutaiton을 사용하여 비동기 통신을 했다. 코드가 줄고 간략해서 좋은 방법인 것 같다. react-query의 장점과 원리에 대해선 추후에 블로그로 더 자세히 다룰 예정이다.
- suspense 적용
react-query 기능 중 하나인 suspense를 사용하여 lodaing을 global로 적용했다. 추후에 error 처리를 할 때에도 사용할 예정이다.
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import Router from "./router/router";
function App() {
const client = new QueryClient({
defaultOptions: {
queries: {
retry: 0,
suspense: true,
},
},
});
return (
<>
<QueryClientProvider client={client}>
<Router />
<ReactQueryDevtools />
</QueryClientProvider>
</>
);
}
export default App;
리펙토링 예정
1. axios intercepter의 사용
axios intercepter는 axios를 사용할때 요청 또는 응답 전에 무언가를 수행하거나 에러가 났을 때 수행할 것들을 미리 정의할 수 있는 axios의 기능이다.
axios intercepter를 사용한 이유
- 배포된 서버에 api를 요청할 때 header에 필수적으로 들어가야하는 부분이 있을 경우, 매번 요청 때마다 header에 값을 입력해줘야 한다. interceptors를 사용해서 api 요청 시 자동으로 해당 값이 들어가서 매 요청 때마다 header에 값을 넣지 않아도 되고, 코드도 간결해지고 복잡성도 줄어든다는 장점이 있었다.
- api를 호출할 때 access token이 만료돼서 데이터를 받아오지 못하는 에러가 있을 때 이를 대비해아여 미리 새로운 access token을 발급받도록 미리 정의하여 실행되지 못했던 api가 호출될 수 있도록 할 수 있다.
2. react-hook-form 라이브러리 사용하기
3. TodoPage 리펙토링
4. TypeScript 제네릭, interface 정리하기
후기 : 관심사의 분리가 은근히 어렵게 느껴졌다. 분리르 어떻게 해야 할지 감이 안 와서 아직 미흡한 것 같지만 분리를 잘하면 가독성도 올라가고 코드가 점점 깔끔해진 것 같아서 뿌듯했다.
'프로그래밍 > React' 카테고리의 다른 글
[React] 리엑트 Hook : useState (0) | 2022.06.27 |
---|---|
[React] caver-js 오류 (1) | 2022.02.23 |