Router를 이용한 접근제한 구현

채희태·2022년 12월 16일

접근 제한

유저의 상태나 권한에 따라 접근할 수 있는 경로를 다르게 설정해 주어야 한다.
서버쪽 뿐만 아니라 1차적으로 프론트 측에서 경로를 다르게 설정하는 것이 좋다.
특히나 나의 경우 서버리스 오픈소스인 supabase를 사용했으므로 프론트에서 확실하게 경로를 설정해야 했다.

구현한 플로우는 다음과 같다.
로그인 -> LocalStorage에 Token 발급 -> Token으로 사용자 유저 정보 조회 -> REDUX user state의 isLoggedIn을 true로 업데이트 -> isLoggedIn의 값이 true일 때 접근할 수 있는 페이지와 접근할 수 없는 페이지 경로를 다르게 설정

PrivateRoute는 로그인한 사용자에게 제공되는 경로이고, 만약 로그인하지 않은 사용자가 이 경로로 접근하고자 한다면 로그인 페이지로 Redirect한다.

반대로, PubilcRoute는 로그인하지 않은 사용자에게 제공되는 루트이다. 만약 로그인한 사용자가 이 경로로 접근하고자 한다면 로그아웃 페이지로 Redirect한다.

구현 방법

참고한 코드

src/utils/isLogin.js

import Cookies from 'js-cookie';

const isLogin = () =>  !!Cookies.get('token')
export default isLogin;

로그인 여부를 검증하는 함수 isLogin을 작성한다.
토큰을 쿠키에 세팅한 경우의 코드인 것 같다.

src/components/PrivateRoute.jsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { isLogin } from '../utils/isLogin';

const PrivateRoute = ({component: Component, ...rest}) => {
    return (
        // Show the component only when the user is logged in
        // Otherwise, redirect the user to /signin page
        <Route {...rest} render={props => (
            isLogin() ?
                <Component {...props} />
            : <Redirect to="/signin" />
        )} />
    );
};

export default PrivateRoute;

src/components/PublicRoute.jsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import isLogin from '../utils/isLogin';

const PublicRoute = ({component: Component, restricted, ...rest}) => {
    return (
        // restricted = false meaning public route
        // restricted = true meaning restricted route
        <Route {...rest} render={props => (
            isLogin() && restricted ?
                <Redirect to="/dashboard" />
            : <Component {...props} />
        )} />
    );
};

export default PublicRoute;

아래와 같이 privateRoute와 PublicRoute컴포넌트를 작성한다.
이 두 컴포넌트는 해당 경로에 맞는 페이지를 렌더링할지, redirect할지 판별한다.

src/App.jsx

import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; // react-router-dom:
import Main from './containers/Main';
import Register from "./containers/Register";
import Login from "./containers/Login";
import MyPage from "./containers/MyPage";
import PublicRoute from "./components/PublicRoute";
import PrivateRoute from "./components/PrivateRoute";
import NotFound from "./containers/NotFound";


const App = () => {
  return (
    <Router>
      <Switch>
        <PublicRoute restricted={false} component={Main} path="/" exact />
        <PublicRoute restricted={true} component={Register} path="/register" exact />
        <PublicRoute restricted={true} component={Login} path="/login" exact />
        <PrivateRoute component={MyPage} path="/mypage" exact /> 
        <Route component={NotFound}/>
      </Switch>
    </Router>
  );
}

export default App;

내가 App컴포넌트는 유저가 들어간 경로에 맞는 컴포넌트(PrivateRoute, PublicRoute 둘 중 하나)를 렌더링하여 컴포넌트가 페이지를 렌더링할지 redirect하게 할지 판별하게 한다.

내가 구현한 코드

우선 위의 참고와 같이 코드를 작성할 수 없는 이유는 다음과 같았다.
1. 위와 같이 props를 ...rest로 받는 코드는 가독성이 좋지 않으며 eslint에서 제한하였다.
2. 나의 경우 토큰이 key가 일정하지 않았으며 REDUX user state의 isLoggedIn의 boolean값으로 렌더링과 Redirect를 판별해야 했다.

src/components/privateRoute.jsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useSelector } from 'react-redux';
import Loading from '../../shared/components/Loading';

// 로그인 되지 않았을 때 접근 가능한 컴포넌트
const PrivateRoute = (props) => {
  const { component, path, exact } = props;
  const { isLoggedIn } = useSelector((state) => state.user);

  if (isLoggedIn === true) {
    return <Route component={component} path={path} exact={exact} />;
  } else if (isLoggedIn === false) {
    return <Redirect to="/" />;
  } else {
    return <Loading />;
  }
};

export default PrivateRoute;

scr/components/PublicRoute.jsx

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useSelector } from 'react-redux';
import Loading from '../../shared/components/Loading';

// 로그인 되지 않았을 때 접근 가능한 컴포넌트
const PublicRoute = (props) => {
  const { component, path, exact } = props;
  const { isLoggedIn } = useSelector((state) => state.user);

  if (isLoggedIn === true) {
    return <Redirect to="/logout" />;
  } else if (isLoggedIn === false) {
    return <Route component={component} path={path} exact={exact} />;
  } else {
    return <Loading />;
  }
};

export default PublicRoute;

위와 같이 구현하였다.
우선 props를 풀어서 깔끔하게 코드를 작성하려고 했다.
또한 초기 마운트 시 redux state에 isLoggedIn이 null로 되어있기 때문에 이 땐 Loading 페이지가 떠 있도록 하였다.
isLoggedIn이 true / false 일 때 경로를 다르게 설정해 주었다.

src/App.js

import React, { useEffect } from 'react';
import { useHistory, Switch, Route } from 'react-router-dom';
import { useDispatch } from 'react-redux';

import supabase from './supabase';
import './App.css';
import {
  loggedInRefreshSessionReducer,
  notLoggedInRefreshSessionReducer,
} from './redux/modules/auth/user';

import Login from './auth/pages/Login';
import Signup from './auth/pages/Signup';
import Permission from './auth/pages/Permission';
import Profile from './profile/page/Profile';
import Users from './users/page/Users';
import PrivateRoute from './auth/components/PrivateRoute';
import PublicRoute from './auth/components/PublicRoute';
import Logout from './auth/pages/Logout';
import Layout from './shared/layout/components/Layout';
import Loading from './shared/components/Loading';
import Dashboard from './dashboard/page/Dashboard';

function App() {
  const history = useHistory();
  const dispatch = useDispatch();
  useEffect(() => {
    // token으로 user 유지
    const refresh = async () => {
      try {
        // token이 application에 있을 경우 (로그인한 경우)
        const { data, error } = await supabase.auth.refreshSession();
        if (error) throw error;
        if (data) {
          // reducer로 보내줄 데이터 생성
          let userData = {};
          // user 테이블을 가져옴.
          const { id } = data.session.user;
          const user = await supabase.from('profiles').select().eq('id', id);
          // permission이 false인 user
          if (!user.data[0].permission) {
            // reducer로 보내줄 데이터 payload
            userData = { userInfo: { ...user.data[0] }, isLoggedIn: true };
            // user 테이블의 permission이 false면 permission 페이지로 이동
            history.push('/permission');
            // redux의 user state를 해당 정보로 업데이트함.
            return dispatch(loggedInRefreshSessionReducer(userData));
          }
          // reducer로 보내줄 데이터 payload
          userData = { userInfo: { ...user.data[0] }, isLoggedIn: true };
          // redux의 user state를 해당 정보로 업데이트함.
          return dispatch(loggedInRefreshSessionReducer(userData));
        }
      } catch (error) {
        console.log(error);
      }
      // token이 application에 없을 경우 (로그인 안한 경우)
      return dispatch(notLoggedInRefreshSessionReducer({ isLoggedIn: false }));
    };
    refresh();
  }, [history, dispatch]);

  return (
    <Switch>
      {/* 나중에 삭제할 부분 (레이아웃 ui) */}
      <Route path="/layout" exact>
        <Layout />
      </Route>
      <Route path="/loading" exact>
        <Loading />
      </Route>
      <PublicRoute component={Login} path="/" exact />
      <PublicRoute component={Signup} path="/signup" exact />
      <PrivateRoute component={Profile} path="/profile" exact />
      <PrivateRoute component={Users} path="/users" exact />
      <PrivateRoute component={Logout} path="/logout" exact />
      <PrivateRoute
        component={Permission}
        path="/permission"
        isLoggedIn
        exact
      />
      <PrivateRoute component={Dashboard} path="/dashboard" exact />
    </Switch>
  );
}

export default App;

useEffect로 초기 렌더링 시 로그인을 유지하는 Refresh 함수를 실행시킨다.

profile
기록, 공부, 활용

0개의 댓글