티스토리 뷰

react router dom으로 중첩 라우팅을 하는 방법은 크게 2가지가 있다.

 

1. 페이지 단위 컴포넌트의 가장 바깥에 (wrapper처럼) 레이아웃 컴포넌트를 감싸서 해당 레이아웃 컴포넌트에서 children props으로 받는 방법 => 레이아웃 컴포넌트를 페이지 단위 컴포넌트 마다 import해줘야 한다. 🔺(번거롭다)

 

2. 레이아웃 컴포넌트에서 자체에서 판단하여 라우팅하는 하기 ✔

  • 해당 방법에서도 내부적으로 2가지 갈래로 중첩라우팅을 표현하는 방법이 다르다. 
  • 아래의 2-1, 2-2 에 자세한 내용을 확인할 수 있다. 
  • 레이아웃 컴포넌트: 부모 컴포넌트, 중첩시킬 라우터의 index에 해당 
  • 페이지 단위 컴포넌트: 자식 컴포넌트, 중첩된 라우터, index/something에 해당

2-1 Outlet 컴포넌트를 사용하여 중첩 라우팅을 표현하는 방법

  • react rounter dom v6은 Outlet이라는 중첩라우터 컴포넌트를 제공하는데, 이 아울렛 컴포넌트는 자식 컴포넌트가 렌더링될 자리를 나타내는 역할을 한다. => 1번 방법의 {children}이 Outlet으로 대체된다고 생각하면 된다.

App.tsx 

import React, { Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';

// TODO: 컴포넌트 lazy load
const LogIn = React.lazy(() => import('@pages/LogIn'));
const SignUp = React.lazy(() => import('@pages/SignUp'));
const WorkSpace = React.lazy(() => import('@layouts/WorkSpace'));
const Channel = React.lazy(() => import('@pages/Channel'));
const DirectMessage = React.lazy(() => import('@pages/DirectMessage'));

const App = () => {
  //어플리케이션이 진입부터 login이 필요함
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/login" element={<LogIn />} />
        <Route path="/signup" element={<SignUp />} />
        <Route path="/workspace" element={<WorkSpace />}>
          <Route path="channel" element={<Channel />} />
          <Route path="dm" element={<DirectMessage />} />
        </Route>
        {/* 기본 redirect */}
        <Route path="/" element={<Navigate to="/login" />} />
        {/* 없는 페이지 접근 */}
        <Route path="*" element={<p>404 Not Found</p>} />
      </Routes>
    </Suspense>
  );
};

export default App;
  • 코드를 보면 중첩 라우팅된 부분은 Routes > Route가 아니라 Route > Route로 되어있는 것을 알 수 있다. 
  • Channel 컴포넌트의 경로는 중첩되어 `/workspace/channel`이 되는 것이다.

Workspace.tsx

// 코드 중략

return (
    <div>
      <Header>test</Header>
      <RightMenu>
        <span>
          <ProfileImg src={gravatar.url(userData.email, { s: '28px', d: 'retro' })} alt={userData.nickname} />
        </span>
      </RightMenu>
      <button onClick={onLogout}>저눈 workspace의 버툰입니당</button>
      <WorkspaceWrapper>
        <Workspaces>안녕</Workspaces>
        <Channels>
          <WorkspaceName>Sleact</WorkspaceName>
          <MenuScroll>이 내부에 Menu컴포넌트</MenuScroll>
        </Channels>
        <Chats>chats</Chats>
        {/* 중첩라우팅 */}
        <Outlet />
      </WorkspaceWrapper>
    </div>
  );
};

export default WorkSpace;

2-2. Routes > Route 컴포넌트를 중첩라우팅이 필요한 컴포넌트에 한번 더 사용하는 방법

  • Outlet을 사용하지 않고 그냥 라우터를 한번 더 정의해주는 방법도 있다. 해당 컴포넌트가 중첩라우팅을 하고 싶다면 그 컴포넌트에 Routes, Route를 활용해서 App.tsx에 들어가는 라우팅처리를 똑같이 해주면 된다. (성능적으로 차이는 없다고 한다.)
  • [2023-06-08 | 여담] 기존 2-1 방법을 사용했을 때, 중첩 라우터의 경로를 제대로 잡아내지 못하는 이슈가 생겼다(내가 잘못 라우팅한 것일수도 있지만 수정을해도 내부 path가 자꾸 없는 페이지로 떴다). 결국 2-2 방법으로 수정하여 라우터를 수정하였더니 원하는 경로가 제대로 잡힐 수 있었다. 

App.tsx

workspace/* 와일드카드가 들어가야한다. Workspace가 레이아웃 컴포넌트 즉 index가 되는 컴포넌트이다.

중요한 점은  /workpsace/:workspace/ 로만 끝내는 것이 아니라 뒤에 /* 라는 `와일드카드` 표기를 해줘야 한다. 그렇지 않으면 Workspace내부에 설정된 중첩라우터의 경로를 제대로 인식하지 못하고 없는 경로로 인식하기 때문이다.

Workspace.tsx

대신 Layout 컴포넌트가 되는 Workspace 컴포넌트에서 라우팅을 해줘야한다.


이렇게 Outlet을 사용하면 props의 타입을 따로 명시해주지 않아도 되는데, 공식 문서의 type 정의를 보면 아래와 같이 나와있다. ReactElement로 타입을 정의해준 이유는 해당 위치에 렌더링되는 자식 컴포넌트가 JSX요소라는 것을 명시적으로 표현하기 위함이다. (중첩 라우팅임을 구조적으로 더 명확하게 표현)

interface OutletProps {
  context?: unknown;
}
declare function Outlet(
  props: OutletProps
): React.ReactElement | null;

아래는 1번방법을 사용했을 때, 직접 명시해준 children 의 prop type이다. 

interface LayoutProps {
  children: ReactNode;
}

ReactNode 타입과 ReactElement 타입

2번 방법인 Outlet을 활용하면 타입 정의도 필요없어진다(react-router-dom에서 TS를 지원하므로). 1번 방법에서 children을 ReactNode로 타입정의해주었지만 타입스크립트에서 ReactNode의 범위는 ReactElement보다 넓다. 

  • ReactNode - React 컴포넌트의 자식 요소로 올 수 있는 모든 유형을 포함, JSX로 작성된 요소, 문자열, 숫자, 배열 등을 포함한다. 자식 요소의 타입에 대해 유연성을 제공
  • ReactElement - JSX로 작성된 요소를 의미, 구체적인 React 컴포넌트 타입과 해당 컴포넌트의 속성(props)를 포함한다. ReactNode 타입보다 더 구체적이며, JSX요소에 대한 타입 정보를 구체적으로 제공한다. 

결론 

두 가지 방법 모두 원하는대로 동작한다. 선택 방법은 크게 라우터의 구조가 계층적으로 되어있다면 2번을 선택하면 되고 라우터의 path가 계층적이지 않다면(일관적이지 않다면) 1번의 방법을 사용하여 children으로 받아서 사용하는 게 좋다. 

 

라우터의 구조가 계층적인 경우

e.g.)

something

   something/main

   something/middle

 

계층적이지 않은 경우

something 

   ws/main   => 여기서 같은 index 라우터 path를 가지고 있지 않기 때문에 계층적이지 않음

   something/middle 

 

댓글