react와 react-router V6로 protected 경로 만들기
이전에 사이드 프로젝트를 만든 사이트를 새롭게 리마스터링 하는 과정에서 protected routing
의 구조가 이상하고 쓰기 불편하다는 느낌이 들어, react-router v6
공식 문서와 여러가지 글들을 찾아보고 최적의 방법을 찾아 정리하였습니다.
이 글은 2024-03-14 에 업데이트 되었습니다. 또한 react-router-v6 6.22.3 버전을 사용하였습니다.
이 글은 HTML, CSS , JavaScript, TypeScript, React, React Router v6에 대한 기본적인 지식을 알고 있어야 합니다. 또한 이 글을 보시기 전 React Router v6 공식 문서에서 기본 사용법에 대한 관련 글을 읽고 보시면 더욱 도움이 됩니다.
Protected Routing 의 중요성
protected routing
은 사용자가 특정 페이지에 접근하기 위해 인증되어야 하는 경우에 사용되는 라우팅 방식입니다. 이는 주로 로그인한 사용자만이 접근할 수 있는 페이지나 권한이 있는 사용자만이 접근 가능한 페이지를 지정하는 데 사용됩니다.
protected routing
은 사용자의 보안과 개인 정보를 보호하는 데 중요한 역할을 하며, 로그인한 사용자만 특정 페이지에 액세스할 수 있도록 제한함으로써 민감한 데이터에 대한 보안을 강화할 수 있습니다.
리팩토링 이전의 Protected Routing
기존 사이드 프로젝트의 Protected Routing 문제점
저는 기존 사이드 프로젝트에서는 사용자가 로그인을 반드시 해야만 홈페이지에 접근할 수 있고 로그인을 하지 않았다면 로그인 페이지로 강제로 이동시키게 만들었습니다.
아래와 같은 코드와 비슷하게 localStorage
혹은 cookie
또는 전역상태를 불러와서 사용할 수 있었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 기존의 코드
// app.tsx
const App = () => {
useEffect(() => {
// 사용자가 로그인을 했는지 확인 (쿠키, 로컬스토리지 등등)
const user = localStorage.get("user");
if (user) {
// 통과
} else {
// 로그인페이지로 리다이렉트
}
}, []);
return <></>;
//.....
};
하지만 위와 같은 코드를 여러 곳에서 작성한다면 사용하기 불편하고 유지보수도 힘들 것 같아 보였습니다.
Hook 으로 해결하기
위의 과정을 hook
으로 중복을 없애 보았습니다. 중복을 없앴지만 여전히 어떤 컴포넌트에서 사용되는지 확인이 불편했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 훅으로 해결하기
// useAuth()
export default function = () => {
const [isUser, setIsUser] = useState(false);
useEffect(() => {
// 사용자가 로그인을 했는지 확인 (쿠키, 로컬스토리지 등등)
const user = localStorage.get("user");
if (user) {
// 통과
} else {
// 로그인페이지로 리다이렉트
}
}, []);
// 유저 정보나 이런 것 등등을 반환...
return {isUser};
};
또 다른 해결 방법
또 다른 해결 방법으로는 Context
를 이용하는 방법으로, <AuthProvider>
를 사용하여 전체 컴포넌트를 감싸서 user
값이 없다면 login
페이지로 이동시키는 방법이 있을 것 같습니다.
이 방법도 상당히 좋은 방법으로, 가독성도 좋고 로그인과 로그아웃 함수를 한번에 관리하기 쉽지만 어디가 보호되는 페이지이고, 어디가 보호되지 않는지 한 눈에 확인하기 어려웠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// AuthProvider
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const login = () => {
// 로그인 처리 로직
setIsLoggedIn(true);
};
const logout = () => {
// 로그아웃 처리 로직
setIsLoggedIn(false);
};
return (
<AuthContext.Provider value=>
{children}
</AuthContext.Provider>
);
};
//...
문제 해결하기
위의 문제를 해결하기 위해서 react-router v6
공식 문서를 찾아보던 도중 객체로 구성된 createBrowserRouter
에 loader
를 사용할 수 있다는 방법을 알게 되었습니다.
react-router 에서의 loader
공식 문서에 의하면 loader
의 역할은 페이지 이동 시 data
를 서버로부터 fetch
하면 받은 데이터를 반환하고 useLoaderData
를 이용하여 해당 페이지에서 사용할 수 있는 함수 입니다.
또한 react-router
공식문서의 예시로는 return
으로 redirect
를 할 수도 있어 다양한 상황에서 응용이 가능합니다.
아래의 기본적인 코드를 보시면 더욱 이해하기 쉽습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// loader로 데이터 받아오기
createBrowserRouter([
{
element: <App />,
path: "/",
loader: async () => {
return fakeDb.from("teams").select("*");
},
children: [
{
element: <Team />,
path: ":teamId",
loader: async ({ params }) => {
return fetch(`/api/teams/${params.teamId}.json`);
},
},
],
},
]);
1
2
3
4
5
6
7
// 사용 시
import { useLoaderData } from "react-router-dom";
export function App() {
// 로더로 부터 받아온 데이터
const loaderData = useLoaderData();
}
사용할 때는 App
컴포넌트에서 useLoaderData()
훅을 이용해 쉽게 사용할 수 있습니다.
우리가 실습할 프로젝트
이제 기본적인 사용법을 알았으니 실제 실습을 통해 사용해 보겠습니다.
요구사항 (필수)
- 로컬스토리지를 이용하여 간단한 로그인 로그아웃 구현하기
- 사용자는 처음에 로컬 스토리지에 유저 데이터가 없을 경우
/login
경로로 이동해야 한다. - 사용자가 로그인 페이지에서 로그인을 하면 다시 홈 페이지로 이동시킨다.
- 홈 페이지는 로그인을 하지 않으면 방문할 수 없고, 로그인 페이지 또한 로그인을 했다면 방문할 수 없다.
요구사항 (선택)
- module.css를 이용하여 로그인 폼 간단히 꾸며보기
라우터 설정하기
저는 리액트에서 전역 설정을 할 때 따로따로 설정하는 것을 좋아하기 때문에 Protected Routes
라는 컴포넌트를 만들어 사용하고 main.tsx
에 적용해 주었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import {
RouterProvider,
createBrowserRouter,
redirect,
} from "react-router-dom";
import App from "./App";
import Login from "./Login";
const ProtectedRoute = () => {
const router = createBrowserRouter([
{
path: "/",
loader: () => {
const user = getUser();
if (!user) return redirect("/login");
return null;
},
element: <App />,
},
{
path: "/login",
loader: () => {
const user = getUser();
if (user) return redirect("/");
return null;
},
element: <Login />,
},
]);
return <RouterProvider router={router} />;
};
export default ProtectedRoute;
// 로컬 스토리지에서 유저를 불러옴
function getUser() {
return localStorage.getItem("user") || null;
}
위의 코드는 다음과 같이 동작합니다.
/
경로로 가기 직전 사용자의 접속 유무를 판단하여 접속 하지 않았다면/login
경로로 리다이렉트 시켜줍니다./login
에서는 이와 반대로 적용시킵니다.
Main.tsx 에 라우터 등록하기
우리가 사용할 최상위 컴포넌트에 등록합니다.
1
2
3
4
5
6
7
8
9
import React from "react";
import ReactDOM from "react-dom/client";
import ProtectedRoute from "./ProtectedRoute.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ProtectedRoute />
</React.StrictMode>
);
로그인 페이지와 홈 페이지 작성하기
로그인 페이지에서는 간단히 로그인을 할 수 있게 그리고 홈 페이지에서는 로그아웃하여 다시 로그인 페이지로 이동시키는 로직을 작성하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// app.tsx
import { useNavigate } from "react-router-dom";
function App() {
const nav = useNavigate();
const handleClick = () => {
localStorage.clear();
nav("/login");
};
return (
<>
<h1>여기는 홈 입니다!</h1>
<p>여기는 로그인 한 사용자가 올 수 없어요</p>
<button onClick={handleClick}>로그아웃</button>
</>
);
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// login.tsx
import { ChangeEvent, useState } from "react";
import styles from "./login.module.css";
import { useNavigate } from "react-router-dom";
function Login() {
const [id, setId] = useState("");
const [password, setPassword] = useState("");
const nav = useNavigate();
const handleIdChange = (e: ChangeEvent<HTMLInputElement>) => {
setId(e.currentTarget.value);
};
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.currentTarget.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 기본 이벤트 방지
// 여기서 폼 제출에 필요한 로직을 추가합니다.
if (!id || !password) return;
localStorage.setItem(
"user",
JSON.stringify({
id: id,
password: password,
})
);
nav("/");
};
return (
<form className={styles.formWrapper} onSubmit={handleSubmit}>
<h1 className={styles.title}>로그인 페이지 입니다</h1>
<p>여기는 로그인한 사용자는 올 수 없어요</p>
<label htmlFor="id" className={styles.label}>
로그인
</label>
<input
className={styles.input}
type="text"
value={id}
onChange={handleIdChange}
name="id"
id="id"
/>
<label className={styles.label} htmlFor="password">
패스워드
</label>
<input
className={styles.input}
value={password}
type="password"
onChange={handlePasswordChange}
name="password"
id="password"
/>
<button className={styles.button}>로그인</button>
</form>
);
}
export default Login;
CodeSandbox에서 결과 확인하기
또는 아래의 사이트에서 결과를 확인하실 수 있습니다
결론
protected route를 설정하는 방법은 여러 가지가 있을 수 있습니다. 오늘 소개한 방법은 createBrowserRouter를 사용하여 경로를 설정하면서 동시에 어떤 경로가 protected하고 어떤 경로가 public한지를 한눈에 파악할 수 있는 장점이 있습니다. 이를 통해 코드의 가독성과 유지보수성을 향상시킬 수 있었습니다.