Post

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 공식 문서에서 기본 사용법에 대한 관련 글을 읽고 보시면 더욱 도움이 됩니다.

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 공식 문서를 찾아보던 도중 객체로 구성된 createBrowserRouterloader를 사용할 수 있다는 방법을 알게 되었습니다.

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()훅을 이용해 쉽게 사용할 수 있습니다.

우리가 실습할 프로젝트

이제 기본적인 사용법을 알았으니 실제 실습을 통해 사용해 보겠습니다.

요구사항 (필수)

  1. 로컬스토리지를 이용하여 간단한 로그인 로그아웃 구현하기
  2. 사용자는 처음에 로컬 스토리지에 유저 데이터가 없을 경우 /login 경로로 이동해야 한다.
  3. 사용자가 로그인 페이지에서 로그인을 하면 다시 홈 페이지로 이동시킨다.
  4. 홈 페이지는 로그인을 하지 않으면 방문할 수 없고, 로그인 페이지 또한 로그인을 했다면 방문할 수 없다.

요구사항 (선택)

  1. 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에서 결과 확인하기

또는 아래의 사이트에서 결과를 확인하실 수 있습니다

Demo

결론

protected route를 설정하는 방법은 여러 가지가 있을 수 있습니다. 오늘 소개한 방법은 createBrowserRouter를 사용하여 경로를 설정하면서 동시에 어떤 경로가 protected하고 어떤 경로가 public한지를 한눈에 파악할 수 있는 장점이 있습니다. 이를 통해 코드의 가독성과 유지보수성을 향상시킬 수 있었습니다.

참고 사이트

https://reactrouter.com/en/main

This post is licensed under CC BY 4.0 by the author.