최근 여러 토이 프로젝트를 진행하고 강의를 들으면서 얻게 된 지식을 공유하고자 한다.
로그인과 관련하여 특정 route에 대한 접근을 제한하는 기능을 구현한 적이 있다.
아니 거의 모든 프로젝트에서 구현을 하고 있고, 잘 구현하고자하는 노력이 있었다.
주로 로그인 전에는 들어갈 수 없는 route에 대한 제한을 걸어두는 식으로 구현을 많이 했었고 나는 다음과 같은 방식을 주로 사용했다.
import { Navigate, Outlet } from "react-router-dom";
const PrivateRoute = () => {
if (userStorage) {
const currentUser = // 현재 유저 상태값
if (!currentUser) {
alert("Log in First");
return <Navigate to="/auth" />;
}
}
return <Outlet />;
};
export default PrivateRoute;
위처럼 따로 route를 막는 컴포넌트를 만들고
index 나 app.tsx 에서
return (
<Routes>
<Route path="/" element={<Navigation />}>
<Route path="/" element={<Main />} />
<Route element={<ProtectRoute />}>
<Route path="/somePath" element={<Something />} />
...등 로그인 후 이용가능한 route
</Route>
</Route>
</Routes>
)
다음과 같이 로그인 후 접근가능한 route를 감쌌다.
최근에 인턴십 과제를 진행하면서 로그인을 하면 signin 과 signup 페이지에 접근하지 못하는 guard도 구현하는 경험을 했다.
위 가드와는 목적이 다르기에 다르지만 비슷한 형태의 가드가 필요했다.
하지만 이는 불필요한 코드의 반복이라고 생각이 들었다.
인턴십 과제를 제출할 때는 다음과 같이 했다.
// 로그인과 관련된 컴포넌트
const currentUser = // 현재 유저 상태값
useEffect(() => {
if (!currentUser) {
navigate('/todo')
}
}, [currentUser])
컴포넌트 내에 다음과 같은 effect 코드가 들어간 형태로 구현했다.
여기서 내가 이 글을 쓰게 된 계기가 나온다.
위와 같이 컴포넌트 내에 route를 바꿔주는 코드를 넣게 되면 컴포넌트가 랜더링이 일어나고 currentUser를 체크하기때문에 유저는 찰나이지만 로그인 컴포넌트의 UI를 보게되고 이는 안좋은 UX를 주게된다.
때문에 고민이 많았고 여러 관련 코드를 접하며 한가지 좋은 방법을 알게 되었다.
react-router-dom 에 있는 함수있다.
공식문서에 있는 글은 다음과 같다.
This is the recommended router for all React Router web projects. It uses the DOM History API to update the URL and manage the history stack.
It also enables the v6.4 data APIs like loaders, actions, fetchers and more.
그리고 다음의 타입을 가진다.
function createBrowserRouter(
routes: RouteObject[],
opts?: {
basename?: string;
future?: FutureConfig;
hydrationData?: HydrationState;
window?: Window;
}
): RemixRouter;
DOM History API를 사용해서 여러 경로를 관리할 수 있게 해주는 아주 유용한 함수되시겠다.
이 함수를 사용해서 각 route를 다루는데 로그인 상태 값을 추가해서 다뤄주면 앞서 고민했던 부분을 구현할 수 있겠다.
기본적으로 CreateBrowserRouter 에 들어갈 router들의 DATA는 다음과 같은 모습이다.
interface RouterBase {
id: number;
path: string;
label: string;
element: React.ReactNode;
withAuth: boolean;
}
여기서 withAuth의 값에 따라 로그인 후 사용가능한지 아닌지를 구분할 수 있다.
true 면 로그인해야만 접근
false 면 로그인하지 않았어야 접근
위 타입을 가지는 형태로 router들을 나누면 다음과 같다.
const routerData: RouterBase[] = [
{
id: 0,
path: "/",
label: "Home",
element: <Home />,
withAuth: false,
},
{
id: 1,
path: "/signin",
label: "로그인",
element: <SignIn />,
withAuth: false,
},
{
id: 2,
path: "/afterlogin",
label: "로그인 후 가능",
element: <Afterlogin />,
withAuth: true,
},
];
이러한 형태의 데이터를 CreateBrowserRouter 함수에 넣어주면 되는데 여기서 약간의 작업이 들어간다.
export const routers = createBrowserRouter(
routerData.map((router) => {
if (router.withAuth) {
return {
path: router.path,
element: <AuthGuardLayout>{router.element}</AuthGuardLayout>,
};
} else {
return {
path: router.path,
element: <PrivateRoute>{router.element}</PrivateRoute>,
};
}
})
);
이름은 임의로 지었다.
위 코드처럼 map을 한번 돌리는데
그 안에서 withAuth가 true면
AuthGuardLayout
을 씌우고
false면PrivateRoute
를 씌운다.
여기서 위 두 함수가 앞서 구현한 privateRoute가 되는 것이다.
안의 코드는 다음과 같다.
const AuthGuardLayout: React.FC<AuthGuardLayoutProps> = ({ children }) => {
const [userProfile, setUserProfile] = useState<string | null>(null);
const { routeTo } = useRouter();
const fetchUserProfile = useCallback(() => {
const userProfileResponse = // 유저 상태 체크
if (userProfileResponse === null) {
routeTo("/signin");
return;
}
setUserProfile(userProfileResponse);
}, [routeTo, setUserToken]);
useEffect(() => {
fetchUserProfile();
}, [fetchUserProfile, routeTo]);
if (userProfile === null) return <Spinner />;
return userProfile ? (
<Navigation>{children}</Navigation>
) : (
<Navigate to="/signin" />
);
};
export default AuthGuardLayout;
유저가 존재하는지 체크하고 route를 바꿔주는 함수를 구현하고
이 함수를 useEffect에 넣어두면
앞서 구현한 컴포넌트와 똑같은 역할을 하게된다.
마지막으로 app.tsx에는
function App() {
return (
<div>
<RouterProvider router={routers} />
</div>
);
}
간단하게 다음과 같이 넣어주면 끝이다.
전체 route에 관한 횡단 관심사 ( 로그인 여부에 따른 페이지 관리 ) 를 따로 router.tsx로 분리해서 관리할 수 있었다.
이로써 컴포넌트 안에서는 route 처리에 대한 부분을 신경쓰지 않아도 되었고
전체 route 또한 따로 array로 관리가 되어 더 가독성 좋게 관리가 되었다.
개인적으로 route에 관한 횡단 관심사 분리가 잘 된 코드라고 생각한다.
다양한 사람들의 코드를 보고 배우며 성장하는 것의 장점이 여실히 드러나는 경험이었다고 생각한다.