원티드 프리온보딩 인턴십을 참가하면서 선발 과제 포함 총 4개의 기업 과제를 수행했습니다. 과정을 진행하면서 스스로 많이 부족하다고 느꼈고, 특히 다른 사람들의 코드를 보면서 이해하는 능력이 필요하다고 생각했습니다. 그래서 수료 이후 코스에 참여한 12팀의 코드를 모두 살펴보면서 배울 점을 기록하려고 합니다.
useEffect
에서 관리하곤 했습니다.useEffect(()=>{
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
if(!토큰) navigate('/');
});
하지만 이는 좋지 않은 코드라는 것을 알게 되었습니다. useEffect
의 콜백은 렌더링된 이후에 실행되기 때문에 권한이 필요한 페이지가 보여진 다음에 로그인 유무를 확인하는 것은 보안상 적절하지 않습니다.
따라서 상위의 라우터 단계에서 먼저 권한 유무를 확인하고 페이지를 적절히 이동시키는 것이 합리적이라고 판단했습니다. 그래서 저는 다른 팀들은 어떻게 라우트를 생성했는지 궁금했습니다.
우선 퍼블릭 라우트와 프라이빗 라우트를 생성하는 방법이 있었습니다.
퍼블릭 라우트에서는 로컬스토리지에 토큰이 존재하면 투두리스트를 보여주는 페이지로 이동하고, 그렇지 않으면 children
을 보여줍니다.
// Public Route
const PublicRoute = ({ children }) => {
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
return 토큰 ? <Navigate to="/todo" /> : children;
};
export default PublicRoute;
children
을, 그렇지 않으면 로그인/회원가입 페이지로 이동합니다.const PrivateRoute = ({ children }) => {
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
return 토큰 ? children : <Navigate to="/" />;
};
export default PrivateRoute;
// App.js
function Router() {
return (
<Routes>
<Route
path="/"
element={
<PublicRoute>
<Home />
</PublicRoute>
}
/>
<Route
path="/todo"
element={
<PrivateRoute>
<Todos />
</PrivateRoute>
}
/>
</Routes>
);
}
export default Router;
따라서 로그인 여부를 확인하는 일을 비즈니스 로직이 포함된 컴포넌트의 상위 컴포넌트인 퍼블릭 라우트와 프라이빗 라우트에 위임할 수 있습니다.
또한 개인적으로 Public
, Private
라는 이름을 통해 로그인과 상관없이 접근 가능한 페이지와 로그인이 필요한 페이지가 명확하게 드러난다는 점에서 이러한 방식이 직관적으로 느껴졌습니다.
리액트 라우터에서 제공하는 기능을 적극적으로 활용한 사례도 있었습니다.
useLocation
, Outlet
그리고 버전 6에서 새롭게 추가된 createBrowserRouter
로 프라이빗 라우팅을 구현한 예시입니다.
퍼블릭 라우터에서는 현재 페이지의 경로를 가져와 토튼이 있으면 원하는 페이지로 이동시키고, 토큰이 존재하지 않으면 자식 라우트의 요소를 보여줍니다. 이를 <Outlet />
컴포넌트로 나타낼 수 있습니다.
const PublicRouter = () => {
const { pathname } = useLocation();
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
if (pathname === '/') {
return 토큰 ? <Navigate to='/todo' replace /> : <Outlet />;
}
return 토큰 ? <Navigate to='/' replace /> : <Outlet />;
};
export default PublicRouter;
const PrivateRouter = () => {
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
return !토큰 ? <Navigate to='/' replace /> : <Outlet />;
};
export default PrivateRouter;
createBrowserRouter
를 통해 라우트를 구성할 수 있습니다.export const router = createBrowserRouter([
{
element: <PublicRouter />,
children: [
{
path: '/'
element: (
<AccountPage />
),
},
],
},
{
element: <PrivateRouter />,
children: [
{
path: '/todo'
element: (
<TodoPage />
),
},
],
},
]);
children
으로 받고 있습니다. 이를 통해 로그인 여부를 확인하는 로직은 비즈니스 로직의 상위 컨텍스트의 라우트로 구현할 수 있다는 점을 알 수 있었습니다.또한 react-router
개발팀에서 제공하는 예시도 확인할 수 있습니다.
react-router/examples/auth 코드 | react-router/examples/auth 코드 예시
아래는 Youtube의 protected routes react router v6
라는 키워드로 검색한 영상입니다. 튜토리얼을 보면서 코드를 익힐 수 있습니다.
React Protected Routes | Role-Based Authorization | React Router v6
The New Way To Create Protected Routes With React Router V6
대부분 Axios
라이브러리를 사용해 HTTP 요청과 관련된 로직을 처리하고 있었습니다.
이번 프로젝트의 경우, 인스턴스를 생성하고 인터셉터를 추가해 요청을 보내기 전에 액세스 토큰의 유무를 검사해 있을 경우에만 요청 헤더에 액세스 토큰을 넣는 코드가 많았습니다.
// 요청을 보내는 루트 URL을 미리 설정하여 인스턴스를 생성
const client = axios.create({
baseURL: process.env.REACT_API_URL,
});
// 인터셉터로 토큰 유무에 따라 요청 헤더를 다르게 추가하는 로직
client.interceptors.request.use((config) => {
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
if (!토큰) {
config.headers['Content-Type'] = 'application/json';
return config;
}
if (토큰 && config.headers) {
config.headers.Authorization = 토큰;
config.headers['Content-Type'] = 'application/json';
return config;
}
});
export default client;
// 인터셉터
apiClient.interceptors.request.use(
(config) => {
const 토큰 = 로컬스토리지에서_토큰을_가져오는_함수();
if (토큰) {
config.headers.Authorization = `Bearer ${accessToken}`;
} else {
delete config.headers.Authorization;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 인터셉터
axiosInstance.interceptors.request.use(config => {
const 토큰 = 로컬스토리지에서_토큰을_불러오는_함수();
if (!토큰) {
config.headers.Authorization = null;
} else {
config.headers.Authorization = `Bearer ${토큰}`;
}
return config;
});
null
과 undefined
인 경우에 값을 할당합니다. 투두리스트 프로젝트에서는 요청 헤더의 Authorization
에 값이 없을 경우 액세스토큰을 넣어 인터셉터의 설정을 추가할 수 있습니다.axiosInstance.interceptors.request.use((config) => {
setAccessToken();
return config;
});
function setAccessToken(config) {
if (!config.headers) {
return config;
}
const bearerToken = `Bearer ${로컬스토리지에서_토큰을_가져오는_함수()}`;
config.headers.Authorization ??= bearerToken;
}