[React+Typescript] 권한 체크 Route(react-router-dom v6)

anonymous-planet·2022년 5월 5일
5

React+Typescript

목록 보기
2/6

이번 포스트는 저번 Private Router에 이어 비슷한 원리로 작동하는 권한을 체크해주는 Router에 대해서 포스팅할 계획입니다.

내게 해당 Router가 필요 했던 이유

  • Login한 사용자의 권한에 따라 접근 가능 여부를 체크하기 위해서

앞서 말한것과 같이 Private Router와 작동 원리는 동일하다고 생각하기 때문에, 권한을 비교해주는 비즈니스 로직의 경우 더 좋은 방법이 많을거라 생각되며 이런식으로 생각할수도 있구나라고 참고만 하면 좋을것 같으며, Router.tsx에서 중첩(?)으로 Component를 쌓는 부분을 Key point로 보면 좋을것 같습니다.

해당 포스트에서 사용하는 권한 Member, Manager, Admin 이라 정하였으며, 권한을 체크해주는 Router를 Protect Route라고 칭하였습니다.

Ex1)
관리자 페이지는 권한이 Manager, Admin의 경우만 접속 가능

Ex2)
관리자 페이지의 회원 관리페이지는 Admin권한을 갖는 로그인한 사용자만 접속 가능

ProtectRoute.tsx

해당 Route의 기능은 메뉴 별로 접근 가능한 사용자의 권한이 다른데
해당 메뉴에 대해서 로그인한 사용자가 접근이 가능한지 여부를 판단해 줍니다.
그렇기 때문에 해당 Route는 메뉴/로그인한 회원의 정보를 필요로 합니다.

import { Category, Member, Role } from '../@types';
import React from 'react';
import { Navigate, Outlet, useLocation } from 'react-router';

/**
 * 해당 Route의 기능은
 * 메뉴 별로 접근 권한이 다른데 해당 메뉴에
 * 접근이 가능한지 판단해주는 Route이다.
 * 로그인한 사용자에대해서 메뉴의 대한 접근 가능 여부를 판단해주기 때문에 
 * 해당 Route는 메뉴에대한 정보와 로그인한 회원의 정보를 필요로 합니다.
 */

// 필요 정보1. 메뉴의 대한 정보(추후 Context를 통해 관리(?))
const category:Array<Category> = 
[
  { 
    idx : 1
    , name : "관리자 메인"
    , path : "/admin"
    , role : [Role.MANAGER, Role.ADMIN]
    , sub : [
            { idx : 2
              , name : "회원 관리"
              , path : "/member"      // sub은 부모의 path가 prefix로 붙는다. 그래서 회원 관리 최종 url은 http://localhost:3000/admin/member가 된다.
              , role : [Role.ADMIN]
              , sub : []
            }
          ]
  }, 
];
export default function ProtectRoute():React.ReactElement|null {

  // 현재 Url정보를 갖고 오기 위해서 useLocation Hooks사용
  const location = useLocation();

  /**
   * 필요정보2. 로그인한 사용자의 정보
   * 로그인을 했고 무조건 데이터가 있다는 가정하에 as 키워드를 붙였다.(as 키워드는 정말 진짜 확실할때만 사용하는걸로 알고 있다.)
   */
  const loginMember:Member = JSON.parse(sessionStorage.getItem("loginMember") as string);


  /**
   * 전체 메뉴에 대한 정보를 새로운 menu라는 상수를 선언해 sub이란 key에 넣어 주었다.
   * sub이라는 key에 넣어준 이유는 sub에 넣어주지 않을경우 2depth부터는 sub에서 꺼내와야하는 문제가 있기 때문이다.
   * 해당 방법은 꼼수일수도....있다.......
   */
  const menu:Category = {...category[0], sub:[...category]};


  /**
   * 현재 URL정보를 split를 이용해 문자열 배열로 정의
   * location.pathname을 할경우 /admin/member 이런식으로 맨앞에 '/'가 붙는다 그렇기 때문에 substring이용해 맨앞에 '/'를 없애 주었다.
   * admin/member로 접속할 경우 location.pathname.substring(1).split("/");를 하면 pathArr에는 ['admin', 'member']가 할당되게 된다.
   */
  let pathArr:Array<string> = location.pathname.substring(1).split("/");

  // 권한 체크(menu : 비교할 메뉴, pathArr:url을'/' split한 문자열 배열, index : pathArr에 값을 꺼내올 index)
  const roleCheck = (menu:Category, pathArr:Array<string>, index:number = 0):Array<Category> => {
 
    /**
     * 여기서 43Line에서 sub에 전체 메뉴를 넣어준 이유가 나온다.
     * sub에 넣어주지 않을 경우 1depth를 비교 할때는 menu에서 비교 해야하고 2depth부터는 menu.sub에서 비교가 이루어져야한다.
     * 그렇게되면 해당 로직이 좀더 복잡해 질 것 같아 43라인같은 방법을 사용했다.
     */
    let result = menu.sub.filter(item => item.role.includes(loginMember.role) && item.path.includes(`/${pathArr[index]}`));
    
    /**
     * url길이를 비교해 한번더 호출될지를 결정한다.
     * url이 http://localhost:3000/admin일 경우는 pathArr의 length가 1이기 때문에 해당 if문은 false가 되고 roleCheck함수는 끝난다.
     * url이 http://localhost:3000/admin/member일 경우는 pathArr의 length가 2이기 때문에 해당 if문은 처음에 true가 되고 roleCheck함수를 한번더 호출한다.
     */
    if(++index < pathArr.length) {
      return roleCheck(result[0], pathArr, index);
    }
    return result;
  }
  // roleCheck함수에 최종 반환 되는건 접근가능한 menu 항목이다.
  let result:Array<Category> = roleCheck(menu, pathArr)

  
  /**
   * roleCheck에서 최종 반환 되는 항목은 접근가능한 menu항목이기 때문에, 
   * result변수의 length가 0일경우는 접근간으한 메뉴가 아니라는 뜻으로 된다.
   */
  if(result.length === 0) {
    alert('해당 페이지 접근 권한이 없습니다! 관리자 메인페이지로 이동합니다.');
    return <Navigate to ="/admin"/>;
  }

  // 접근 가능한 페이지일 경우 해당 페이지를 보여준다.
  return <Outlet />
}

Router.tsx

현재 구현된 ProtextRouter의 경우는 로그인 한 사용자의 권한을 무조건 요구하기 때문에 PrivateRoute(authentication=true)인 Route안에 반드시 종속되어야한다.

import {BrowserRouter, Routes, Route} from 'react-router-dom';
import LoginPage from '../pages/LoginPage';
import MainPage from '../pages';
import MyPage from '../pages/MyPage';
import PrivateRoute from './PrivateRoute';
import AdminMainPage from '../pages/admin';
import ProtectRoute from './ProtectRoute';
import MemberManageMainPage from '../pages/admin/member';
import Error404 from 'pages/common/error/Error404';


export default function Router() {

  return (
    <BrowserRouter>
      <Routes>

        {/* 인증 여부 상광 없이 접속 가능한 페이지 정의 */}
        <Route index element={<MainPage/>}/>

        {/* 인증을 반드시 하지 않아야만 접속 가능한 페이지 정의 */}
        <Route element={<PrivateRoute authentication={false}/>}>
          <Route path="/login" element={<LoginPage/>} />
        </Route>

        {/* 인증을 반드시 해야지만 접속 가능한 페이지 정의 */}
        <Route element={<PrivateRoute authentication={true}/>}>
          <Route path="/mypage" element={<MyPage/>} />

          {/* 권한 체크가 필요한 페이지 정의 */}
          {/* ProtectRoute는 반드시 로그인한 사용자의 한해서만 되도록 구현되어 PrivateRoute안에 종속되어야한다. */}
          <Route element={<ProtectRoute/>}>
            <Route path="/admin" element={<AdminMainPage/>}/>
            <Route path="/admin/member" element={<MemberManageMainPage/>}/>
          </Route>
        </Route>

        {/* 인증/권한 여부와 상관 없이 접근 가능한 Error 페이지 정의 */}
        <Route path='/*' element={<Error404/>}/>
      </Routes>
    </BrowserRouter>
  )
}

AdminMainPage.tsx

해당 페이지는 Login한 사용자의 권한이 MANAGER, ADMIN을 요구하는 페이지 이다.
간단하게 정보만 보여주도록 되어있다.

import { Member } from '@types';
import { useNavigate } from 'react-router';
import { Link } from 'react-router-dom';

/**
 * 관리자 메인화면
 * 1. 로그인을 반드시 해야지만 접근 가능한 페이지
 * 2. Role이 MANAGER, ADMIN 일 경우만 접근 가능
 */
export default function AdminMainPage() {
  const navigate = useNavigate();

  const loginMember:Member = JSON.parse(sessionStorage.getItem('loginMember') as string);

  const logoutHandler = () => {
    sessionStorage.setItem('isAuthenticated', 'false');
    sessionStorage.setItem('loginMember', '');
    navigate('/login');
  }
  return (
    <div>
      <h1>관리자 메인화면</h1>
      <h2>{loginMember.name} 님이 로그인 하셨습니다.(권한 : {loginMember.role})<button onClick={logoutHandler}>로그아웃</button></h2>
      <Link to = "/admin/member">회원 관리</Link>
    </div>
  );
}

MemberManageMainPage.tsx

해당 페이지는 Login한 사용자의 권한이 ADMIN을 요구하는 페이지 이다.
간단하게 정보만 보여주도록 되어있다

import { Member } from '@types'
import { useNavigate } from 'react-router';

/**
 * 회원 관리 메인 화면
 * 1. 로그인을 반드시 해야지만 접근 가능한 페이지
 * 2. Role이 ADMIN일 경우만 접근 가능 
 */
export default function MemberManageMainPage() {

  const navigate = useNavigate();

  
  const loginMember:Member = JSON.parse(sessionStorage.getItem('loginMember') as string);


  const logoutHandler = () => {
    sessionStorage.setItem('isAuthenticated', 'false');
    sessionStorage.setItem('loginMember', '');
    navigate('/login');
  }
  return (
    <div>
      <h1>회원 관리 메인화면</h1>
      <h2>{loginMember.name} 님이 로그인 하셨습니다.(권한 : {loginMember.role})<button onClick={logoutHandler}>로그아웃</button></h2>
    </div>
  )
}

이상으로 간단하게 권한을 체크하는 Route를 작성해 보았습니다.
(Type등과 같이 이외에 항목들이 포함되어 있는 전체 소스는 github를 참고해주세요.)

해당 소스에 계속해서 추가로 포스팅을 진행할 예정입니다!!!

GitHub : https://github.com/anonymous-planets/anonymous-planet-react

profile
취미로 Front-End를 즐기는 Back-End개발자 입니다.

2개의 댓글

comment-user-thumbnail
2022년 6월 6일

안녕하세요 행성님~
저는 처음 프로젝트를 프론트엔드 팀에서 맡아 진행 중인 디발자를 꿈꾸는 박찬민 입니다.
행성님의 이 포스트를 통해 굉장한 도움을 받고 코드를 짜고 있는데요,
혹시 시간이 있으시다면 꼭 좀 같이 팀프로젝트를 진행해 보고 싶습니다.

sessionStorage 사용이며, router의 깔끔함 까지..
혹시 디스코드를 하신다면 Charming Park#1532 를 찾아주세요
간절히 대화를 나누고 싶습니다!!!

1개의 답글