(React Router + Typescript) 액세스 권한과 중첩된 라우팅

  • by

시작하기 전에

부족한 부분은 언제든지 지적하십시오.

이 기사는 대답이 아니며 올바른 길을 보여주지 않습니다.

공부한 내용을 정리한 문장이므로 참고하십시오.


이 문서에서는 react-router를 사용하여 로그인 사용자의 권한에 따라 페이지 액세스 승인/거부 기능을 만듭니다.

+ 라우터 라이브러리는 React-router v6을 기반으로합니다.

v5와 약간 다르기 때문에 변경 사항은 공식 문서를 참조하십시오.

React-Router 공식 문서

필요한 기능 정리

로그인한 경우와 로그인하지 않았을 때 액세스할 수 있는 페이지를 나누어

로그인한 경우 권한이 있는지 여부에 따라 액세스 가능한 페이지를 다시 분할하고 액세스할 수 없는 경우 기존 페이지로 이동하는 기능입니다.

위 내용을 정리하면 다음과 같습니다.

1. 로그인하지 않으면 액세스할 수 없는 페이지

2. 로그인하지 않으면 액세스하기 쉬운 페이지

2-2. 로그인해야 하며 + 권한이 있는 경우에만 액세스할 수 있는 페이지

3. 잘못된 액세스를 시도한 경우 액세스 가능한 페이지로 이동하거나 액세스할 수 없는 가이드 페이지를 표시합니다.

플로우차트를 작성하면 다음과 같이 됩니까? 아마 옳을 것입니다.

허;;


라우터 만들기

1. 로그인 토큰 관리

먼저 사이트에 액세스할 때 token(로그인했는지)이 있는지 확인합니다.

로그인은 토큰을 이용하여 관리하게 되었습니다.

sessionStorage와 localStorage의 두 가지 중에서 고민했지만 내가 만들고 싶은 기능에 필요한 만료일을 설정하기 위해 localStorage를 사용했습니다.

(쿠키를 사용한 로그인 관리는 제외되었습니다.

)

다음은 예입니다.

저장된 토큰은 다음과 같이 저장하고 필요에 따라 다른 정보를 넣으십시오.

expire가 만료 날짜를 설정한 값입니다.

// localStorage

token = {
    "value": "token",
    "userName": "kim",
    "sort": "00",
    "expire": 1676597289995
}

다음 makeLoginToken 함수는 로그인 시 서버에서 받은 정보로 토큰을 생성합니다.

이 시점에서 만료일을 설정하기 위해 tts을 설정합니다.

예제 코드에서는 하루를 ms로 단위를 변경한 시간 86400000으로 설정했습니다.

다른 시간이 필요한 경우 Google에서 단위 변경을 검색하면 쉽게 찾을 수 있습니다.

// makeLoginToken.tsx

interface makeLoginToken_TYPE {
  tokenKeyName: string;
  keyValue: string;
  userName: string;
  sort: "00" | "01";
}

export default function makeLoginToken({
  tokenKeyName = "token",
  keyValue,
  userName,
  sort,
}: makeLoginToken_TYPE) {
  const tts = 86400000; // 1day/ms
  const tokenValue = {
    value: keyValue,
    userName,
    sort,
    expire: Date.now() + tts, // 토큰 생산일 기준 + 1일 만료기간
  };
  const objectToString = JSON.stringify(tokenValue);
  localStorage.setItem(tokenKeyName, objectToString);
}

첫 번째 사이트에 액세스하거나 페이지를 이동할 때마다 loginTokenExpiry 함수를 통해 토큰의 유무와 만료 날짜를 확인합니다.

1. 토큰이 존재하면서 유효한 경우 true를 반환합니다.

2. 토큰없거나 만료된 경우 토큰 삭제그리고 false를 반환합니다.

// loginTokenExpiry.ts
// 토큰 유무 확인 후 유효기간 체크

export default function loginTokenExpiry(tokenKeyName = "token") {
  const localLoginToken = localStorage.getItem(tokenKeyName);

  if (!
localLoginToken) return false; const StringToObject = JSON.parse(localLoginToken); if (Date.now() > StringToObject.expire) { localStorage.removeItem(tokenKeyName); return false; } else return true; }

2. LoginPrivateRoute

두 개의 PrivateRoute를 만듭니다.

먼저 로그인 페이지에 액세스할 때 작동하는 LoginPrivateRoute는 다음과 같이 실행됩니다.

1. 위의 loginTokenExpiry 함수를 실행하여 토큰 상태를 확인합니다.

2. loginTokenExpiry 반환 값을 기반으로 redirect() 실행 및 구성 요소 반환 결정

3. redirect() 를 실행하면 토큰의 sort 를 조회해, 「00」, 「01」의 경우는 해당의 url 에 이동, 양쪽 모두 「/permission」에 이동

4. 의 경우, 대응하는 기능을 실행

*<アウトレット/>무엇인지는 아래에서 설명합니다.

// LoginPrivateRoute.tsx

export function LoginPrivateRoute() {
  const isAuthenticated = loginTokenExpiry();
  return isAuthenticated ? redirect() : <Outlet />;
}

const redirect = () => {
  const loginMember = JSON.parse(localStorage.getItem("token") as string);

  if (loginMember.sort === "00") return <Navigate to="/management" />;
  else if (loginMember.sort === "01") return <Navigate to="/user" />;
  else return <Navigate to="/permission" />;
};

2-2 LoginPrivateRoute 사용 예

LoginPrivateRoute는 다음과 같이 element={ } 내에서 사용됩니다.

이렇게 작성하면 login 페이지에 액세스 할 때마다 LoginPrivateRoute의 함수가 실행되어 어떻게 작동하는지 결정합니다.

여기서 token이 없으면 element={}로 이동

토큰이 있으면 토큰 sort를 확인하고 해당 페이지로 이동

여기서 LoginPrivateRoute는} />로 이동하는 것이 무엇인지 모르는 경우가 있습니다.

이 부분이의 기능이기 때문에, 그렇게 넘어서 뒤에서 나온다.

기능 참조

// App.tsx
export default function App() {
  return (
    <Routes>
      <Route element={<LoginPrivateRoute />}>
        <Route index={true} element={<Login />} errorElement={<NotFound />} />
      </Route>
    </Routes>
  );
}

3. PrivateRoute

나머지 PrivateRoute는

더 간단한 기능을 수행합니다.

토큰이 있는지 확인하는 경우 아울렛이 없으면 ‘/’로 보냅니다.

export function PrivateRoute(): JSX.Element {
  const isAuthenticated = loginTokenExpiry();
  return isAuthenticated ? <Outlet /> : <Navigate to="/" />;
}

3-2. PrivateRoute의 사용 예

이제 LoginPrivateRoute 1개와 PrivateRoute 2개가 있습니다.

앞으로 로그인, 메인 등 페이지로 이동할 때마다 3개 PrivateRoute가 어디로 보내는지 확인하고 적절한 위치로 이동합니다.

export default function App() {
  return (
    <Routes>
      <Route element={<LoginPrivateRoute />}>
        <Route index={true} element={<Login />} />
      </Route>

      <Route element={<PrivateRoute />}>
        <Route element={<ProtectRoute access={"00"} />}>
          <Route path="/management" element={<Management />} >
            <Route index={true} element={<Admin />} />
            <Route path="/management/register" element={<Register />} />
            <Route path="/management/license" element={<License />} />
          </Route>
        </Route>
      </Route>

      <Route element={<PrivateRoute />}>
        <Route element={<ProtectRoute access={"01"} />}>
          <Route path="/user" element={<User />} >
            <Route index={true} element={<UserAdmin />} />
            <Route path="/user/register" element={<UserRegister />} />
            <Route path="/user/account" element={<UserAccount />} />
          </Route>
        </Route>
      </Route>

      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

그 안에 ProtectRoute를 가지고, 그 안에 Route를, 그리고 그 안에 Route 복수를 가지는 것을 확인할 수 있습니다.

이 구조는 내가 궁극적으로 만들려는 형태입니다.

LoginPrivateRoute에서 토큰이 있다면 sort에 따라 페이지를 이동한다고 말했습니다.

각각 “/management”와 “/user”로 이동하지만, 이 두 URL을 가진 Route는 ProtectRoute를 가진 Route가 감싸고 PrivateRoute가 감싸고 있습니다.

구조가 이렇게 “/management”로 이동하면, “/management”를 감싸고 있는 최상위 Route인 PrivateRoute가 실행되고, 다음에 ProtectRoute를 가지는 Route가 실행되는 형태입니다.

아래 코드는 사실상 LoginPrivateRoute와 동일한 기능을 수행하는 PrivateRoute입니다.

isAuthenticated 반환 값이 true이면를 false로 설정하면 “/”(로그인 페이지)로 이동합니다.

4. Outlet이란?

그러면에 대해 간략하게 설명합니다.

router가 지원하는 기능입니다.

react
children의 역할과 같다고 생각합니다.


위의 사진과 같은 레이아웃이 있는 페이지가 있을 때 왼쪽 sideMenu가 계속 유지하면서 차트 영역만 계속 변경할 때 Management.tsx 마찬가지로 코드를 작성합니다.

다음 코드는 SideMenu 구성 요소가 정적으로 배치되는 동안 아울렛 영역만 변경됩니다.

변경되는 레이아웃은 다음 App.tsx에서 확인할 수 있습니다.

// Management.tsx

function Management() {
  return (
    <>
      <SideMenu />
      <Outlet />
    </>
  );
}

Management를 element로 갖는 Route가 있습니다.

이 루트 중 Admin, Register, License를 element로 가지며 path가 있는 Route가 다시 세 가지 있습니다.

이 3개의 Route(element)는 모두 Management 콘센트입니다.

Outlet이라는 컴포넌트 안에 3개의 컴포넌트(path)가 있어, 유저가 브라우저에 url 를 입력하면, 그 url 와 일치하는 Route 를 Outlet 가 return 합니다.

if 등을 사용하여 조건부로 렌더링하는 것과 같습니다.

그래서 쉽게 생각하면 아울렛은 라우터의 아이들입니다!
이런 식으로 알아야합니다.

자세한 내용은 공식 문서를 참조하십시오.

// App.tsx

export default function App() {
  return (
      <Route path="/management" element={<Management />} >
        <Route index={true} element={<Admin />} />
        <Route path="/management/register" element={<Register />} />
        <Route path="/management/license" element={<License />} />
      </Route>
  );
}

5. ProtectRoute 및 Route index

다시 기존 코드로 돌아가서 두 개의 PrivateRoute를 살펴 보았습니다.

그건 그렇고 PrivateRoute의 경우,}>와 같은 Route가 있습니다.

이 루트는 권한이 있으면 관리자 페이지에, 그렇지 않으면 사용자 페이지에만 액세스할 수 있습니다.

아래 코드를 보면 ProtectRoute는 access를 매개 변수로 사용하고 token의 “sort”와 비교하여

sort가 access와 같으면 Outlook으로 이동합니다.

일치하지 않으면 sort에 맞는 페이지로 이동합니다.

그렇지 않지만 위의 내용에 해당하지 않는 경우 액세스할 수 없다는 메시지를 표시하는 빈 페이지로 이동합니다.

// ProtectRoute.tsx

interface PROTECT_ROUTE_TYPE {
  access: string;
}

export default function ProtectRoute({ access }: PROTECT_ROUTE_TYPE): React.ReactElement | null {
  const loginMember = JSON.parse(localStorage.getItem("token") as string);

  if (loginMember.sort === access) return <Outlet />;
  else {
    if (loginMember.sort === "00") return <Navigate to="/management" />;
    else if (loginMember.sort === "01") return <Navigate to="/user" />;
    else return <Navigate to="/permission" />;
  }
}

아래의 App.tsx를 보면 첫 번째 경로에 세 개의 경로가 있습니다.

이 중 path 가 없는 index 가 있는 Route 는, 「/management」로 액세스했을 경우에 나타내는 default 페이지입니다.

index를 사용하지 않는 경우 /management”로 연결하면 sideMenu만 표시되고 아울렛 영역은 빈 페이지로 표시됩니다.

admin에 액세스하려면 /management/admin을 입력해야 합니다.

이 부분은 만들려는 기능에 따라 선택적으로 사용할 수 있습니다.

// App.tsx의 일부분

  <Route path="/management" element={<Management />} >
    <Route index={true} element={<Admin />} />
    <Route path="/management/register" element={<Register />} />
    <Route path="/management/license" element={<License />} />
  </Route>
  
  
  // index를 안쓸경우
  <Route path="/management" element={<Management />} >
    <Route path="/management/admin" element={<Admin />} />
    <Route path="/management/register" element={<Register />} />
    <Route path="/management/license" element={<License />} />
  </Route>

최종 코드

다음 코드는 최종 코드입니다.

상기에 없었던 errorElement 라고 하는 것이 생겼습니다만, 이 컴퍼넌트는 그 페이지에 액세스 했습니다만, 어떤 이유로 error가 발상한다고 보여주는 페이지입니다.

// App.tsx
// 아래의 코드 url에는 파라미터와 쿼리스트링을 사용하지 않았습니다.

실제로는 사용해야합니다.

export default function App() { return ( <Routes> <Route element={<LoginPrivateRoute />}> <Route index={true} element={<Login />} errorElement={<NotFound />} /> </Route> <Route element={<PrivateRoute />}> <Route element={<ProtectRoute access={"00"} />}> <Route path="/management" element={<Management />} errorElement={<NotFound />}> <Route index={true} element={<Admin />} errorElement={<NotFound />} /> <Route path="/management/register" element={<Register />} errorElement={<NotFound />} /> <Route path="/management/license" element={<License />} errorElement={<NotFound />} /> </Route> </Route> </Route> <Route element={<PrivateRoute />}> <Route element={<ProtectRoute access={"01"} />}> <Route path="/user" element={<User />} errorElement={<NotFound />}> <Route index={true} element={<UserAdmin />} errorElement={<NotFound />} /> <Route path="/user/register" element={<UserRegister />} errorElement={<NotFound />} /> <Route path="/user/account" element={<UserAccount />} errorElement={<NotFound />} /> </Route> </Route> </Route> <Route path="*" element={<NotFound />} /> // 위에서 설정한 route외 접근시도 할 경우 </Routes> ); }
// ProtectRoute.tsx

interface PROTECT_ROUTE_TYPE {
  access: string;
}

export default function ProtectRoute({ access }: PROTECT_ROUTE_TYPE): React.ReactElement | null {
  const loginMember = JSON.parse(localStorage.getItem("token") as string);

  if (loginMember.sort === access) return <Outlet />;
  else {
    if (loginMember.sort === "00") return <Navigate to="/management" />;
    else if (loginMember.sort === "01") return <Navigate to="/user" />;
    else return <Navigate to="/permission" />;
  }
}
// PrivateRoute.tsx

export function PrivateRoute(): JSX.Element {
  const isAuthenticated = loginTokenExpiry();
  return isAuthenticated ? <Outlet /> : <Navigate to="/" />;
}

export function LoginPrivateRoute() {
  const isAuthenticated = loginTokenExpiry();
  return isAuthenticated ? redirect() : <Outlet />;
}

const redirect = () => {
  const loginMember = JSON.parse(localStorage.getItem("token") as string);

  if (loginMember.sort === "00") return <Navigate to="/management" />;
  else if (loginMember.sort === "01") return <Navigate to="/user" />;
  else return <Navigate to="/permission" />;
};