React 개인 과제 라우팅 설정 - 로그인 사용자만 이용 가능하도록 보호
TIL 23. 12. 04
들어가며
개인 과제의 필수 구현 사항에서 “로그인 해야만 팬레터 화면으로 진입 가능”이라는 항목이 있었다. 로그인 상태인 경우만 홈, 상세, 프로필 화면에 접근할 수 있고, 로그아웃 상태에서는 로그인 화면에만 접근할 수 있도록 구현해야 한다.
라우팅 설정
1. 프로젝트에 라우터 적용
BrowserRouter
라는 컴포넌트를 사용하여 최상위 컴포넌트를 감싸준다. BrowserRouter
는 HTML5 History API를 사용하여 브라우저의 URL을 동적으로 변경하고 관리할 수 있다.
History API는
history
전역 객체를 통해 브라우저 세션 히스토리에 대한 접근을 제공합니다. 사용자의 방문 기록을 앞뒤로 탐색하고, 방문 기록 스택의 내용을 조작할 수 있는 유용한 메서드와 속성을 노출합니다. - History API - MDN
// src/index.js
import App from 'App';
import React from 'react';
import ReactDOM from 'react-dom/client';
import store from 'redux/config/configStore';
import { Provider } from 'react-redux';
import { injectStore } from 'apis/api';
import { BrowserRouter } from 'react-router-dom';
injectStore(store);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>,
);
2. 페이지 컴포넌트 생성
각 페이지에서 사용할 컴포넌트를 만든다. 디렉토리 구조의 일관성을 위해 /pages
디렉토리에서 페이지 컴포넌트들을 관리한다. 현재 프로젝트에서는 5개의 페이지를 사용한다.
src/pages/Home.jsx
src/pages/Detail.jsx
src/pages/Login.jsx
src/pages/NotFound.jsx
src/pages/Profile.jsx
(+) index.js를 사용해 import 코드 줄이기
자바스크립트 모듈 시스템에서 index.js
파일은 각 디렉토리의 진입점으로 사용된다. 다른 파일에서 pages
디렉토리를 import할 때 pages
디렉토리의 index.js
파일을 찾아서 해당 파일에서 내보낸 모듈들을 가져온다. 이를 활용하면 라우터 설정을 할 때 페이지 컴포넌트들을 import하는 코드가 간단해진다.
// src/pages/index.js
import Home from 'pages/Home';
import Login from 'pages/Login';
import Detail from 'pages/Detail';
import NotFound from 'pages/NotFound';
import Profile from 'pages/Profile';
export { Home, Login, Detail, NotFound, Profile };
// BEFORE
import Login from 'pages/Login';
import Home from 'pages/Home';
import Detail from 'pages/Detail';
import Profile from 'pages/Profile';
import NotFound from 'pages/NotFound';
// AFTER
import { Detail, Home, Login, NotFound, Profile } from 'pages';
3. 라우트 정의
Routes
는 특정 경로에 대한 라우팅 규칙이 정의된 여러 Route
를 포함하고, 가장 일치하는 컴포넌트를 렌더링한다. 브라우저의 URL과 Route
에 정의된 경로가 일치하면 Route
에 지정된 컴포넌트가 렌더링된다.
Routes
: 브라우저의 URL 경로에 따라 가장 적합한 Route를 찾아 해당 컴포넌트를 렌더링Route
: 정의한 URL과 일치하는 경로에 접근할 때 렌더링할 컴포넌트를 지정path
: 현재 URL과 비교할 URL 경로element
: 렌더링할 컴포넌트
각 페이지 컴포넌트는 다음과 같은 경로로 접근하면 렌더링된다. 팬레터 상세 페이지에서는 선택한 팬레터만 나타난다. URL의 경로에서 팬레터의 id로 하나의 팬레터를 특정하는데, 동적 세그먼트를 활용한다. /detail/:id
경로로 접근하면 url의 id를 가진 팬레터만 사용자에게 보여준다.
/
: 메인 페이지/login
: 로그인 페이지/detail/:id
: 팬레터 상세 페이지/profile
: 프로필 페이지
// src/Router.js
import { Route, Routes } from 'react-router-dom';
import { Detail, Home, Login, NotFound, Profile } from 'pages';
export default function Router() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/detail/:id" element={<Detail />} />
<Route path="/profile" element={<Profile />} />
<Route path="/login" element={<Login/>} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
4. 보호된 라우트 (중첩 라우팅)
로그인한 사용자만 서비스를 이용할 수 있게 하러면 로그인 페이지와 지정된 경로 외의 페이지에 접근을 차단해야 한다. 사용자가 권한이 있는지 확인하고, 권한이 있는 경우에만 서비스를 이용하도록 한다.
react-router-dom 라이브러리는 중첩 라우팅을 지원한다. 이때 라우트는 부모-자식 관계를 가지며, 경로가 일치하면 부모 Route
의 element에 정의된 컴포넌트가 먼저 렌더링되고 자식 Route
의 경로와 일치하는 컴포넌트가 렌더링된다.
중첩 라우팅: 하나의 페이지나 컴포넌트 안에서 또 다른 라우팅 구조를 만드는 것
<Route element={<ProtectedRoute isLogin={isLogin} />}>
<Route path="/" element={<Home />} />
<Route path="/detail/:id" element={<Detail />} />
<Route path="/profile" element={<Profile />} />
</Route>
예를 들어 /
경로에 접근하면 ProtectedRoute
가 먼저 렌더링되고, 그 안에서 /
경로에 매칭되는 <Home />
컴포넌트가 렌더링된다.
// src/components/ProtectedRoute.jsx
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
export default function ProtectedRoute({ isLogin }) {
if (!isLogin) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
Navigate
: 다른 경로로 리다이렉션하고 페이지를 이동시키는 데 사용되는 컴포넌트to
: 리다이렉션 경로replace
: true로 설정하면 브라우저 history를 추가하지 않고 이동한 페이지로 교체 (기록)
Outlet
: 중첩된 라우트가 있다면 자식 라우트에 매칭된 컴포넌트를 렌더링
ProtecteRoute
컴포넌트는 전달 받은 isLogin
prop으로 사용자 권한을 확인하고 로그인한 사용자이면 자식 Route
를 렌더링하고 로그인하지 않았다면 /login
경로로 이동시킨다. 단순히 로그인 여부를 확인하여 경로를 변경하고 자식 라우트를 렌더링하는 역할이다.
왜 useNavigate 아닌 Navigate?
Navigate
와 useNavigate
모두 페이지를 이동하고 경로를 변경하는 데 사용한다. 차이점은 Navigate
는 컴포넌트이고 useNavigate
는 hook이다.
Navigate
컴포넌트는 주로 조건부 렌더링 시 사용한다. 예를 들어, 특정 조건(여기서는 로그인)을 만족하지 않을 때 리다이렉션을 시킬 수 있다. useNavigate
는 주로 이벤트 핸들러 내에서 사용한다. 버튼 클릭과 같은 사용자의 행동에 반응하여 페이지를 이동할 때 유용하다.
Note: This API is mostly useful in React.Component subclasses that are not able to use hooks. In functional components, we recommend you use the
useNavigate
hook instead. -참고 : 이 API는 후크를 사용할 수 없는 React.Component 하위 클래스에서 유용합니다. 기능적인 컴포넌트의 경우useNavigate
후크를 사용하는 것을 권장합니다. - Navigate v6.20.1
▶ useNavigate
훅 사용
// src/components/ProtectedRoute.jsx
import React, { useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
export default function ProtectedRoute({ isLogin }) {
const navigate = useNavigate();
useEffect(() => {
if (!isLogin) {
return navigate('/login', { replace: true });
}
}, [isLogin, navigate]);
return <Outlet />;
}
useNavigate
를 사용해 페이지를 이동시킬 때는 useEffect
훅을 같이 사용해야 했다.
Warning: You should call navigate() in a React.useEffect(), not when your component is first rendered.
useEffect
는 컴포넌트의 렌더링이 끝난 후 실행된다. 그래서 화면이 잠깐 보였다가 페이지가 변경된다. 물론, isLogin
값으로 화면이 보이지 않게 할 수 있으나 { isLogin && <Outlet /> }
추가적인 코드가 많이 필요하다고 생각해 Navigation
컴포넌트를 사용하였다.
Navigation
컴포넌트는 렌더링될 때 리다이렉션을 수행한다. 따라서, 컴포넌트가 렌더링되면 즉시 렌더링을 중지하고 새로운 경로로 이동한다. 로그인 여부를 판단해 리다이렉션을 수행해야 하는 목표에 더 부합하다고 여겨진다.
▶ Navigation
컴포넌트 사용
// src/components/ProtectedRoute.jsx
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
export default function ProtectedRoute({ isLogin }) {
if (!isLogin) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
5. 최종 라우터 설정
ProtectedRoute
컴포넌트를 활용해 사용자 인증이 필요한 라우트를 보호한다. isLogin
prop을 받아 사용자의 로그인 상태를 확인하고 로그인 되어 있으면 자식 라우트를, 로그인되어 있지 않으면 로그인 페이지로 이동시킨다. 어떤 경로에도 매칭되지 않으면 NotFound
컴포넌트가 렌더링된다.
사용자의 권한을 확인하는 로직은 작성되어 있고 Redux로 사용자 정보를 상태 관리하고 있다.
// src/Router.js
import { Route, Routes } from 'react-router-dom';
import ProtectedRoute from 'components/ProtectedRoute';
import { Detail, Home, Login, NotFound, Profile } from 'pages';
import { useSelector } from 'react-redux';
export default function Router() {
const isLogin = useSelector(({ auth }) => auth.isAuthenticated);
return (
<Routes>
<Route element={<ProtectedRoute isLogin={isLogin} />}>
<Route path="/" element={<Home />} />
<Route path="/detail/:id" element={<Detail />} />
<Route path="/profile" element={<Profile />} />
</Route>
<Route path="/login" element={<Login isLogin={isLogin} />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
/login
경로에서 렌더링 되는 Login 컴포넌트도 isLogin
prop을 전달받아 로그인 되어 있다면 Navigation
컴포넌트를 사용해 메인 페이지로 이동하도록 구현하였다.
import React, { useState } from 'react';
import { Navigate } from 'react-router-dom';
export default function Login({ isLogin }) {
if (isLogin) {
return <Navigate to="/" replace />;
}
// ... 생략
}
결과
느낀점
지난 팀 과제에서도 로그인한 사용자만 마이페이지와 글을 작성할 수 있게 하는 기능을 구현했었다. 보호되어야 하는 라우트마다 ProtectedRoute
를 중첩하여 사용하는 방식으로, 해당 라우트에 접근할 때마다 로그인 여부를 확인했었다.
<Routes>
<Route path="/" element={<Home />} />
<Route path="/boards" element={<AllBoard />} />
<Route path="/boards/:id" element={<BoardDetail />} />
<Route path="/boards/new" element={<ProtectedRoute><NewBoard /></ProtectedRoute>}/>
<Route path="/mypage" element={<ProtectedRoute><MyPage /></ProtectedRoute>}/>
// ... 생략
</Routes>
이번 과제에서는 전역적인 로그인 상태에 따른 라우팅이 필요해 ProtectedRoute
컴포넌트 하나로 여러 라우트를 보호하는 방식으로 구현했다. 프로젝트의 기능, 구조에 따라 선택해야겠지만 두 가지 라우트 보호 방식을 비교해보면 다음과 같다.
각 라우트에 대한 보호 | 전체 라우트 보호 |
---|---|
- 장점: 라우트에 대한 접근을 개별적으로 설정 가능 - 단점: 코드가 반복되어 길어짐 |
- 장점: 보호되는 라우트가 변경될 때 전체를 수정할 필요가 없어 유지보수가 편리 - 단점: 특정 라우트 예외 처리의 어려움 |
전체 라우트 보호 기능을 구현하면서 React Router의 중첩 라우팅과 전역 상태를 활용한 라우트 보호에 대한 이해가 높아졌다. 이후의 프로젝트에서 더 나은 설계와 방법을 고민하며 적용해 볼 수 있는 폭이 넓어진 계기가 되었다.
댓글남기기