오늘로 개인 프로젝트가 완료되었다. 개인 프로젝트의 종료와 동시에 튜터님의 샘플 코드가 제공된 관계로 "못난 내 코드" 라는 이름으로 본인의 코드의 부족한 점을 파악하고 이를 어떻게 개선할 수 있을지 알아보는 시간을 가지기로 한다.
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="detail/:id" element={<Detail />} />
<Route path="profile" element={<Profile />} />
</Route>
<Route path="login" element={<Login />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
export default function Router() {
const isLogin = useSelector((state) => state.auth.isLogin);
return (
<BrowserRouter>
<Routes>
{isLogin ? (
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/detail/:id" element={<Detail />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<Navigate replace to="/" />} />
</Route>
) : (
<>
<Route path="/login" element={<Login />} />
<Route path="*" element={<Navigate replace to="/login" />} />
</>
)}
</Routes>
</BrowserRouter>
);
}
isLogin 상태를 이용한 라우터 분기 처리.
내 코드에서는 로그인 상태에 따른 라우터의 분기가 아예 이루어지고 있지 않다. 그렇다고 로그인 하지 않은 사용자를 Home이나 Detail 이나 Profile 페이지로 접근시키는 것은 아니고 따로 각 페이지에서 사용자의 accessToken의 유효성을 검사하여 경고 팝업을 띄우고 useNavigate로 Login 페이지로 이동시키는 접근을 취하고 있다.
이렇게 했을때 문제점은 크게 세가지.
1) 일단 Home이나 Profile 로 접근하는것 자체는 막을 수 없다는 것.
2) 그리고 사용자가 뒤로가기를 눌러서 다시 잘못된 접근을 시도할 수 있다는 것.
3) 각 페이지에서 불필요하게 사용자 accessToken을 검사해야 한다는 것.
이에 반해 모범 코드에서는 isLogin 상태와 삼항연산자를 이용해 아예 처음부터 분리되어 있는 컴포넌트 트리를 그려내고 있다.
Navigate 컴포넌트 활용과 path="*"
Navigate 컴포넌트의 활용은 처음 보는 것으로 이번 기회에 반드시 익히고 넘어가야 한다. 기본적으로 const navigate= useNavigate()
와 유사한 느낌으로 동작하는데, 컴포넌트가 렌더링 될때 to 에 지정된 주소로 이동시킨다. 모범 코드에서는 로그인 된 상태에서는 Home으로 이동시키고 로그인되지 않은 상태일 경우 Login 페이지로 이동시키고 있다.
path="*"
은 모든 경로를 의미한다. 즉 주소창에 무엇이 입력되었든 무조건 Navigate 컴포넌트가 렌더링 되고 이 Navigate 컴포넌트에 의해 각각 Home 과 Login 으로 이동하게 된다.
replace
사용
Navigate 가 useNavigate 훅과 비슷한 컴포넌트라는것 까지는 이해했는데 replace가 생소했다. 찾아보니 replace 옵션은 "히스토리 스택에서 현재 페이지를 대체한다" 고 한다. 표현이 좀 복잡한데 아주 간단하게 설명하면 사용자가 뒤로가기로 개발자의 의도에서 벗어난 경로로 이동하는 것을 봉쇄하겠다는 의미이다.
뒤로가기 버튼을 누르면 이전 히스토리에 남아있는 페이지로 이동하게 되는데 replace 옵션은 이 이전 히스토리를 날려버린다고 생각해도 좋다.
//src>utils>tokenValid.js
import { jwtInstance } from "../axios/api";
//accessToken 의 유효성을 검사해서 true/false를 리턴하는 함수
const tokenValid = async (accessToken) => {
try {
await jwtInstance.get(`/user`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
});
return true;
} catch (error) {
console.error(error);
return false;
}
};
export default tokenValid;
authApi.interceptors.request.use(
(config) => {
// 헤더에 토큰 넣기
const accessToken = localStorage.getItem("accessToken");
if (accessToken) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
}
return config;
},
(err) => {
return Promise.reject(err);
}
);
authApi.interceptors.response.use(
(response) => {
return response;
},
(err) => {
toast.error(err.response.data.message);
if (
err.response.data.message ===
"토큰이 만료되었습니다. 다시 로그인 해주세요."
) {
// 로그아웃처리
return store.dispatch(logout());
}
return Promise.reject(err);
}
);
jsonApi.interceptors.request.use(
async (config) => {
const { data } = await authApi.get("/user");
if (data.success) return config;
},
(err) => {
return Promise.reject(err);
}
);
interceptor 활용
나는 인터셉터를 거의 활용하지 못해서 accessToken 검증하는 로직을 함수로 따로 빼고 매번 사용할 때마다 import 해서 사용했다. 그렇지만 모범 코드에서는 interceptor 를 활용하여 api 요청을 할때 자동으로 검증이 이루어지고, 또 그 이후 에러 처리까지 한번에 하는 모습이 보인다.
특히 jsonApi의 인터셉터 내에서 authApi 를 사용하는 것이 가능한줄 몰랐는데 이번에 알게되었다. 모범 코드에서는 jsonApi에서 요청을 보낼때 authApi.get 으로 유효성 검사를 먼저 하고 이때 오류가 나는 경우에 대해서는 authApi의 인터셉터에서 처리하도록 하고 있다.