[React] URL을 통한 직접 접근 제한하기(유저 인증을 통한 접근 제한)

박기영·2023년 1월 19일
1

React

목록 보기
24/32

react-router-dom으로 Route를 만들고 그 곳에서 자유롭게 오가는건 구현하기 쉽다.
단, 로그인 기능이 추가되는 순간부터 고려해야할 것이 생긴다.
URL에 경로를 입력해서 인가되지 않은 사용자가 접근하는 것을 막아줘야하는 것이다.
예를 들어, 로그인한 유저가 로그인 페이지나 회원가입 페이지에 접근하는 것이 있겠다.

유저 인증 활용 구조

우선, 어떤 상황에 어떤 컴포넌트가 막혀야하고, 보여야하는지를 생각해보자.

필자가 현재 만들고 있는 프로젝트의 페이지는 크게 다음과 같이 나눌 수 있다.

1. 누구나 접근 가능
LandingPage : 랜딩 페이지. 사이트 접속 시 첫 화면.
CompanyPage : 회사 소개 페이지.
ArtistPage : 회사 소속 아티스트 소개 페이지.
BusinessPage : 자회사 등 소개 페이지.

2. 로그인한 유저만 접근 가능
MyPage : 좋아요 누른 아티스트를 볼 수 있는 페이지.

3. 로그인 하지 않은 유저만 접근 가능
LoginPage : 로그인 페이지.
JoinPage : 회원 가입 페이지.

유저 인증 방법

필자는 localStorage에 저장해놓은 토큰을 contextAPI를 통해 모든 컴포넌트에서 접근하도록 했다.
토큰의 존재 여부로 로그인 상태를 판단할 것이다.

// App.tsx

import React, { Suspense } from "react";
import { Routes, Route, BrowserRouter } from "react-router-dom";

import { AuthContext } from "./context/auth-context";
import { useAuth } from "./hoc/auth-hook";
import LoadingSpinner from "./shared/LoadingSpinner";
import AnyRoute from "./routes/AnyRoute";
import PrivateRoute from "./routes/PrivateRoute";
import PublicRoute from "./routes/PublicRoute";
import UnValidPage from "./pages/UnValidPage/UnValidPage";

const LandingPage = React.lazy(() => import("./pages/LandingPage/LandingPage"));
const CompanyPage = React.lazy(() => import("./pages/CompanyPage/CompanyPage"));
const ArtistPage = React.lazy(() => import("./pages/ArtistPage/ArtistPage"));
const BusinessPage = React.lazy(
  () => import("./pages/BusinessPage/BusinessPage")
);
const MyPage = React.lazy(() => import("./pages/MyPage/MyPage"));
const LoginPage = React.lazy(() => import("./pages/LoginPage/LoginPage"));
const JoinPage = React.lazy(() => import("./pages/LoginPage/JoinPage"));

function App() {
  const { token, login, logout, userId } = useAuth();

  return (
    <BrowserRouter basename={process.env.PUBLIC_URL}>
      <AuthContext.Provider
        value={{ isLoggedIn: !!token, token, userId, login, logout }}
      >
        <Suspense fallback={<LoadingSpinner />}>
          <Routes>
			// 각종 Route가 들어갈 곳
          </Routes>
        </Suspense>
      </AuthContext.Provider>
    </BrowserRouter>
  );
}

export default App;

구조 파악이 끝났으니 하나씩 구현해보자!

로그인 유저만 접근 가능

// PrivateRoute.tsx

import React, { useContext } from "react";
import { Outlet, Navigate } from "react-router-dom";
import { AuthContext } from "../context/auth-context";

// 로그인 유저만 접근 가능
// 비로그인 유저 접근 불가
const PrivateRoute = () => {
  const auth = useContext(AuthContext);

  if (!auth.isLoggedIn) {
    alert("로그인이 필요한 기능입니다.");
  }

  return auth.isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
};

export default PrivateRoute;

로그인한 유저는 토큰을 발급 받으므로 isLoggedIntrue이다.
따라서, isLoggedInfalse인 유저가 해당 라우터에 접근하면 알림을 띄워주고,
Navigate를 통해 로그인 페이지로 이동시킨다.

로그인한 유저라면 알림은 보이지 않을 것이고, Outlet에 컴포넌트가 표시될 것이다.

적용한 모습은 다음과 같다.

// App.tsx

<Route element={<PrivateRoute />}>
  <Route path="/mypage/:userId" element={<MyPage />} />
</Route>

중첩 라우트를 활용한 것으로,
MyPage 컴포넌트는 PrivateRouteOutlet에 해당하게 된다.

PrivateRoute를 통해서 MyPage에 접근하므로
로그인하지 않은 유저가 MyPage에 접근하면, 알림을 보게 될 것이고,
로그인 페이지로 이동하게 될 것이다.

비로그인 유저만 접근 가능

// PublicRoute.tsx

import React, { useContext } from "react";
import { Outlet, Navigate } from "react-router-dom";
import { AuthContext } from "../context/auth-context";

// 비로그인 유저만 접근 가능
// 로그인 유저 접근 불가
const PublicRoute = () => {
  const auth = useContext(AuthContext);

  return auth.isLoggedIn ? <Navigate to="/" /> : <Outlet />;
};

export default PublicRoute;

비로그인 유저만 접근이 가능하게 하는 것은 방금 살펴본 것의 정반대로 해주면 된다.
isLoggedInfalse인 경우에는 Outlet을 통해 컴포넌트를 보여주고,
그렇지 않은 경우(로그인한 경우)에는 Navigate를 통해 랜딩페이지로 이동시킨다.

비로그인 유저가 페이지를 이동할 때마다 로그인해달라고 알림을 보여주는 것은 좋지않다고 생각하여,
알림 기능은 사용하지 않았다.

적용한 코드는 다음과 같다.

<Route element={<PublicRoute />}>
  <Route path="/login" element={<LoginPage />} />
  <Route path="/join" element={<JoinPage />} />
</Route>

이미 로그인을 한 유저는 LoginPage, JoinPage에 접근하려고 한다면,
PublicRoute 내에 작성된 코드에 따라 랜딩페이지로 이동하게 될 것이다.

누구나 접근 가능

// AnyRoute.tsx

import React from "react";
import { Outlet } from "react-router-dom";

// 비로그인, 로그인 유저 전부 접근 가능
function AnyRoute() {
  return <Outlet />;
}

export default AnyRoute;

그렇다면, 누구나 접근 가능한 페이지를 만드는 것은 간단하다.
아무런 조건을 설정하지 않으면 그만이다.
...사실 굳이 이 기능은 만들지 않아도 된다.
다만, 코드가 눈에 딱 보이게 구분되어 있으면 좋으니까 필자는 굳이 이 기능을 만들었다.

적용된 코드는 아래와 같다.

<Route element={<AnyRoute />}>
  <Route path="/" element={<LandingPage />} />
  <Route path="/company" element={<CompanyPage />} />
  <Route path="/artist" element={<ArtistPage />} />
  <Route path="/business" element={<BusinessPage />} />
</Route>

왜 중첩 라우터로 사용하는걸까?

혹시나, 글을 읽으시는 분들 중에 중첩 라우터가 아니라, 아래 코드와 같이
Route를 반환하는 컴포넌트를 사용하면 더 깔끔해보이지 않을까 생각하는 분이 계실 것 같다.

// App.tsx

import React, { Suspense } from "react";
import { Routes, Route, BrowserRouter } from "react-router-dom";

// ... //

function App() {
  const { token, login, logout, userId } = useAuth();

  return (
    <BrowserRouter basename={process.env.PUBLIC_URL}>
      <AuthContext.Provider
        value={{ isLoggedIn: !!token, token, userId, login, logout }}
      >
        <Suspense fallback={<LoadingSpinner />}>
          <Routes>
            <TestRoute path="/" element={<LandingPage />} />
          </Routes>
        </Suspense>
      </AuthContext.Provider>
    </BrowserRouter>
  );
}

export default App;
// TestRoute.tsx

import React from "react";
import { Route } from "react-router-dom";

function TestRoute(props: any) {
  return <Route path={props.path} element={props.element} />;
}

export default TestRoute;

TestRoute 컴포넌트가 컴포넌트를 받아 Route에 적용해서 반환하면 되는거 아닐까?
훨씬 깔끔해보이는데?
하지만...이는 에러가 발생한다.

참고 이미지

RoutesRoute 혹은 React.Fragment만을 자식으로 갖기 때문이다.
따라서, 중첩 라우터를 활용하는 것이 좋아보인다.
필자가 알아본 react-router-dom v6를 사용한 해당 기능 구현에서는 이 방법이 최선으로 보인다.

결론

지금까지 구현한 기능을 통해, 특정 조건을 만족하지 않는 유저의 Route 접근 제한을 배웠다.
로그인한 유저가 URL에 로그인 페이지 경로를 입력해서 들어오는 것을 막는 등
다양한 방법으로 활용할 수 있을 것이다.

참고 자료

Bikash Raj Sharma님 블로그
Vijit Ail님 게시글
woolta님 블로그

profile
나를 믿는 사람들을, 실망시키지 않도록

1개의 댓글

comment-user-thumbnail
2023년 1월 19일

안녕하세요

답글 달기