[React] react-router-dom v6

김채운·2024년 4월 8일
0

React

목록 보기
25/26

최근에 react-router-dom을 사용할 일이 있어서 찾아보니, 그 전에 v5를 사용했을 때와는 다르게 업데이트 된 내용과 새로운 사용법이 올라와서 벨로그에 작성을 해두고자 한다.

➡️ createBrowserRouter

  • createBrowserRouter는 모든 React Router 웹 프로젝트에 권장되는 라우터입니다. DOM History API를 사용하여 URL을 업데이트하고 기록 스택을 관리합니다.
  • createBrowserRouter는 React Router v6에서 새롭게 도입된 API 중 하나로, 라우터 객체를 생성하는 데 사용됩니다. 이 함수는 라우터 구성을 객체 형태로 선언적으로 만들 수 있게 해주며, 이를 BrowserRouter 컴포넌트에 전달하여 라우팅을 설정합니다. createBrowserRouter를 사용하면 route에 대한 상세한 정의와 route에 대한 여러 가지 추가 설정을 할 수 있습니다.

✨ DOM History API

DOM History API는 브라우저의 세션 히스토리를 조작할 수 있는 JavaScript API입니다. 이 API를 사용하면 브라우저의 히스토리 스택을 조작하고, 사용자가 이전 페이지로 이동하거나 새로운 페이지로 이동할 수 있습니다.

DOM History API에는 다음과 같은 주요 메서드와 이벤트가 포함됩니다:

  • pushState(state, title, url): 새로운 상태를 히스토리 스택에 추가하고, 지정된 URL로 이동합니다.
  • replaceState(state, title, url): 현재 상태를 변경하고, 지정된 URL로 이동합니다.
  • popstate 이벤트: 사용자가 뒤로가기 또는 앞으로 가기 버튼을 클릭할 때 발생하는 이벤트입니다.
  • history.back(): 뒤로가기 버튼을 눌러 이전 페이지로 이동합니다.
  • history.forward(): 앞으로 가기 버튼을 눌러 다음 페이지로 이동합니다.
  • history.go(n): 지정된 숫자 만큼 페이지를 앞이나 뒤로 이동합니다.
    이러한 메서드와 이벤트를 사용하여 JavaScript로 브라우저의 히스토리를 동적으로 제어할 수 있습니다. 이것은 단일 페이지 애플리케이션(SPA)과 같은 웹 애플리케이션에서 페이지 전환 및 상태 관리에 유용합니다.

➡️ RouterProvider

모든 데이터 router 객체는 RouterProvider컴포넌트에 전달되어 앱을 렌더링하고 나머지 데이터 API를 활성화합니다.

Type declaration

declare function RouterProvider(
  props: RouterProviderProps
): React.ReactElement;

interface RouterProviderProps {
  fallbackElement?: React.ReactNode;
  router: Router;
  future?: Partial<FutureConfig>;
}
  • 해당 코드는 TypeScript를 사용하여 React 애플리케이션에서 라우터를 제공하는 RouterProvider 컴포넌트와 관련된 선언을 보여줍니다. 이 코드는 라우터의 제공 및 설정을 위한 프로바이더 역할을 수행하는 컴포넌트의 타입 선언을 정의합니다.

1. declare function RouterProvider(props: RouterProviderProps): React.ReactElement;

  • RouterProvider 함수가 선언되어 있습니다.
    이 함수는 RouterProviderProps라는 인자를 받으며, 이 인자의 형태는 아래에 정의되어 있습니다.
    이 함수는 React 엘리먼트를 반환합니다.

2. interface RouterProviderProps { ... }:

  • RouterProvider 함수의 인자로 사용될 RouterProviderProps 인터페이스가 정의되어 있습니다.
    이 인터페이스는 fallbackElement, router, future라는 세 가지 속성을 가지고 있습니다.
    fallbackElement는 선택적으로 React 노드를 받습니다.
    router는 Router 타입의 객체여야 합니다.
    future는 부분적으로 설정된 FutureConfig 타입의 객체입니다.

3. Partial(FutureConfig):

  • FutureConfig의 일부 속성만을 포함하는 타입입니다. 즉, future 속성은 선택적으로 설정할 수 있습니다.
    이 코드는 React 애플리케이션에서 라우터를 설정하는 데 사용되는 타입 선언이며, RouterProvider 컴포넌트의 props의 형태를 정의하고 있습니다. 이를 통해 타입스크립트를 사용하여 라우터 프로바이더 컴포넌트를 사용할 때 타입 검사를 수행할 수 있습니다.

Partial Type

Partial이란?
Partial은 TypeScript에서 제공하는 타입 유틸리티 함수로, 주어진 타입의 모든 프로퍼티를 optional하게 만들어주는 기능을 제공합니다. 즉, 주어진 타입의 각 프로퍼티에 ?를 붙여서 각 프로퍼티를 optional하게 만든 새로운 타입을 만들어줍니다. 예를 들어,
interface Person {
    name: string;
    age: number;
    address: string;
}
type PartialPerson = Partial<Person>;

위와 같이 정의하면, PartialPerson은 Person 타입의 name, age, address 프로퍼티를 각각 optional하게 만든 타입이 됩니다. 즉,

let john: PartialPerson = {
    name: 'John Doe'
};

위와 같이 john 변수는 name 프로퍼티만 가지고 있으면서도 올바른 타입입니다.

✔️ fallbackElement

fallbackElement는 React Router v6의 RouterProvider 컴포넌트의 옵션 중 하나입니다. 이는 서버에서 애플리케이션을 렌더링하는 동안 네트워크가 느릴 수 있는 경우 사용자에게 로딩 상태를 표시하는 데 사용됩니다. 예를 들어, 초기 렌더링이 완료될 때까지 사용자에게 로딩 스피너나 로딩 메시지를 표시하는 데 사용될 수 있습니다.

<RouterProvider
router={router}
fallbackElement={<SpinnerOfDoom />}
/>
  • 이렇게 하면 사용자가 초기 렌더링이 완료될 때까지 어떤 작업이 진행 중인지 알 수 있고, 로딩 상태에 대한 피드백을 제공할 수 있습니다. 이는 사용자 경험을 개선하고 애플리케이션의 완료되지 않은 상태에 대한 혼란을 줄이는 데 도움이 됩니다.

✔️ future

future는 React Router의 RouterProvider 컴포넌트에서 사용되는 옵션 중 하나입니다. 이 옵션은 애플리케이션에서 사용할 Future Flags(미래 플래그)를 설정하는 데 사용됩니다. Future Flags는 새로운 기능이나 변경된 동작을 활성화하거나 비활성화하는 데 사용되는 설정입니다.

function App() {
return (
  <RouterProvider
    router={router}
    future={{ v7_startTransition: true }}
  />
);
}

예를 들어, React Router의 다음 버전(v7)에서 도입될 수 있는 새로운 기능을 미리 사용해보고 싶다면 v7_startTransition Future Flag를 활성화할 수 있습니다. 이렇게 하면 애플리케이션이 React Router v7의 새로운 기능을 미리 사용할 수 있으며, 이를 통해 실제 릴리스에 대비하여 마이그레이션을 쉽게 할 수 있습니다.

위의 예시에서는 future 옵션을 사용하여 v7_startTransition: true를 설정하여 React Router v7의 startTransition 기능을 사용하도록 설정하였습니다.

startTransition은 React Router v7에서 도입될 예정인 비동기적 라우팅을 위한 새로운 API 중 하나입니다. 이를 통해 라우팅 전환을 시작할 때 사용자에게 더 나은 경험을 제공할 수 있게 됩니다.

따라서 future 옵션을 사용하여 애플리케이션에서 새로운 기능을 미리 활성화할 수 있으며, 이는 애플리케이션의 업그레이드 및 마이그레이션을 보다 원활하게 만드는 데 도움이 됩니다.

import * as React from "react";
import * as ReactDOM from "react-dom";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";

import Root, { rootLoader } from "./routes/root";
import Team, { teamLoader } from "./routes/team";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    loader: rootLoader,
    children: [
      {
        path: "team",
        element: <Team />,
        loader: teamLoader,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

➡️ Route

Routes는 React Router 앱에서 가장 중요한 부분 중 하나입니다. URL 세그먼트를 components, data loading 그리고 data mutations에 연결합니다. router중첩을 통해 복잡한 애플리케이션 레이아웃과 데이터 종속성이 단순해지고 선언적이 됩니다. Routes는 router생성 function에 전달되는 객체입니다.
이 말은 경로들이 URL 세그먼트를 컴포넌트, 데이터 로딩 및 데이터 변형과 결합시킨다는 것입니다. 예를 들어, /about 경로는 About 컴포넌트를 렌더링하고, 해당 컴포넌트에서 데이터를 로드하거나 변경하는 데 사용될 수 있습니다.

경로의 중첩을 통해 복잡한 레이아웃을 표현할 수 있습니다. 예를 들어, /users 경로 아래에 /users/:id 경로를 중첩시킬 수 있고, 각 경로에 대해 다른 컴포넌트와 데이터 로딩 로직을 정의할 수 있습니다.

이러한 경로 설정은 React Router를 통해 애플리케이션의 네비게이션과 뷰의 구조를 선언적으로 정의할 수 있게 해줍니다.

const router = createBrowserRouter([
  {
    // it renders this element
    element: <Team />,

    // when the URL matches this segment
    path: "teams/:teamId",

    // with this data loaded before rendering
    loader: async ({ request, params }) => {
      return fetch(
        `/fake/api/teams/${params.teamId}.json`,
        { signal: request.signal }
      );
    },

    // performing this mutation when data is submitted to it
    action: async ({ request }) => {
      return updateFakeTeam(await request.formData());
    },

    // and renders this element in case something went wrong
    errorElement: <ErrorBoundary />,
  },
]);

Type declaration

interface RouteObject {
  path?: string;
  index?: boolean;
  children?: React.ReactNode;
  caseSensitive?: boolean;
  id?: string;
  loader?: LoaderFunction;
  action?: ActionFunction;
  element?: React.ReactNode | null;
  hydrateFallbackElement?: React.ReactNode | null;
  errorElement?: React.ReactNode | null;
  Component?: React.ComponentType | null;
  HydrateFallback?: React.ComponentType | null;
  ErrorBoundary?: React.ComponentType | null;
  handle?: RouteObject["handle"];
  shouldRevalidate?: ShouldRevalidateFunction;
  lazy?: LazyRouteFunction<RouteObject>;
}

✔️ Path

말 그대로 주소이다. URL 패턴에 해당하는 path가 있는 컴포넌트가 렌더링 된다. 여기서 페턴이라고 하는 이유는 URL 주소가 정확히 일치할 수도 있지만 그렇지 않은 경우도 있기 때문이다. 어떤 패턴이 있는지 알아보자.

Dynamic Segments

Dynamic Segments는 경로 세그먼트가 :로 시작한다. 이것은 해당 경로 세그먼트가 동적이며 URL과 일치하는 값이 파싱되어 다른 라우터 API에 params로 제공될 것임을 의미합니다. 그래서 dynamic segments는 useParams() API를 통해 불러와 사용할 수 있다.
예를 들어, /teams/:teamId와 같은 경로 패턴은 /teams/team1, /teams/team2 등과 같은 URL에 모두 일치한다. 이때 :teamId는 Dynamic Segments이며, 해당 URL에 있는 실제 값인 "team1"이나 "team2"와 같은 값으로 파싱됩니다.

<Route
  // this path will match URLs like
  // - /teams/hotspur
  // - /teams/real
  path="/teams/:teamId"
  // the matching param will be available to the loader
  loader={({ params }) => {
    console.log(params.teamId); // "hotspur"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Team />}
/>;

// and the element through `useParams`
function Team() {
  let params = useParams();
  console.log(params.teamId); // "hotspur"
}

Optional Segments

Optional Segments는 세그먼트 끝에 ?를 추가하여 만들 수 있습니다. 이는 해당 세그먼트가 선택적임을 나타냅니다. 이 또한 useParams() API를 통해 불러와 사용할 수 있습니다.

<Route
  // this path will match URLs like
  // - /categories
  // - /en/categories
  // - /fr/categories
  path="/:lang?/categories"
  // the matching param might be available to the loader
  loader={({ params }) => {
    console.log(params["lang"]); // "en"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Categories />}
/>;

// and the element through `useParams`
function Categories() {
  let params = useParams();
  console.log(params.lang);
}

예를 들어, /:lang?/categories와 같은 경로 패턴은 /categories, /en/categories, /fr/categories와 같은 URL에 모두 일치할 수 있습니다. 이때 :lang은 Optional Segments이며, 해당 URL에 있는 값이 파싱될 수도 있고 없을 수도 있습니다.

이러한 Optional Segments는 라우팅을 보다 유연하게 만들어줍니다. URL의 특정 부분이 존재하지 않을 수도 있는 경우에 유용하게 사용될 수 있습니다. 예를 들어, 다국어 지원이 필요한 경우에는 위의 예시와 같이 언어 코드가 선택적으로 포함될 수 있습니다.

Splats

catchall 또는 star segments으로도 불리는 splats는 패턴으로, 경로 패턴이 /*로 끝나면 해당 경로는 / 다음의 모든 문자와 일치하게 됩니다.

<Route
  // this path will match URLs like
  // - /files
  // - /files/one
  // - /files/one/two
  // - /files/one/two/three
  path="/files/*"
  // the matching param will be available to the loader
  loader={({ params }) => {
    console.log(params["*"]); // "one/two"
  }}
  // and the action
  action={({ params }) => {}}
  element={<Team />}
/>;

// and the element through `useParams`
function Team() {
  let params = useParams();
  console.log(params["*"]); // "one/two"
}

예를 들어, /files/와 같은 경로 패턴은 /files, /files/one, /files/one/two, /files/one/two/three 등과 같은 URL에 모두 일치합니다. 이때 는 Splats 세그먼트로, 해당 URL의 나머지 부분을 나타냅니다.

function Home () {
	const { org, "*": splat } = useParams();
	// ...
}

Splats 세그먼트를 비구조화 할당을 통해 새 이름으로 지정할 수 있습니다. 이를 통해 splat세그먼트에 쉽게 접근하고 처리할 수 있습니다.

즉, 위의 코드에서 params 객체에서 org와 [*]키를 가진 속성을 추출하여 각각 org와 splat 변수에 할당하고 있습니다. 이때 splat은 [*] 키를 가진 속성의 값입니다.
이러한 비구조화 할당을 통해 특정 키를 가진 속성을 쉽게 추출하여 사용할 수 있습니다. 위의 예시에서는 * 키를 가진 속성을 splat이라는 변수에 할당하여 나중에 사용할 수 있도록 하고 있습니다. 일반적으로 변수 이름은 splat이라고 한다.

Layout Routes

만약 path를 생략하게 된다면 이 route가 "Layout Routes"가 됩니다. UI 중첩에 참여하지만 URL에 세그먼트를 추가하지 않습니다. 이는 단지 UI layout를 위한 route가 됩니다.

<Route
  element={
    <div>
      <h1>Layout</h1>
      <Outlet />
    </div>
  }
>
  <Route path="/" element={<h2>Home</h2>} />
  <Route path="/about" element={<h2>About</h2>} />
</Route>
  • 위의 코드에서는 Route 컴포넌트를 사용하여 Layout Routes를 정의하고 있습니다. 이 라우트는 path prop을 생략하고 있습니다. 따라서 이 라우트는 어떠한 URL 경로와도 일치하지 않습니다. 이 element는 단지 UI layout를 위한 route가 됩니다.

  • Layout Routes의 element prop은 레이아웃을 구성하는 JSX 요소를 포함합니다. 위의 예시에서는 Layout이라는 h1텍스트와 Outlet이 포함되어 있습니다. Outlet은 React Router의 특별한 컴포넌트로, 자식 라우트의 UI가 렌더링되는 위치를 나타냅니다.

  • 그리고 Route 컴포넌트 내부에 자식으로 Route 컴포넌트들이 포함되어 있습니다. 이 자식 Route 컴포넌트들은 Layout Routes의 자식 라우트로서, Layout Routes의 Outlet에 의해 렌더링됩니다.

  • 예를 들어, /about 경로로 접근하면 Layout Routes의 h1텍스트인 Layout과 함께 h2텍스트인 About이 Outlet에 의해 렌더링됩니다. 따라서 Layout Routes는 UI 중첩을 구성하는 역할을 하면서 URL에는 영향을 주지 않습니다.

  • 이런 특징을 가진 Layout Routes는 특정 페이지마다 공통으로 가지는 Layout이 있을 때 유용하게 사용할 수 있습니. 예를 들어 모든 페이지에 Header 컴포넌트가 있다고 하면 Layout Routes에 Header 컴포넌트를 렌더링 하고 Outlet를 사용하면 됩니다.

Index

index 라우트는 특정 경로에 일치하는 자식 경로가 없을 때 렌더링되는 기본 자식 경로입니다. 즉, 부모 경로가 일치하지만 자식 경로가 일치하지 않을 때 렌더링됩니다. 이러한 인덱스 라우트는 부모 경로의 기본 UI를 채우는 데 사용됩니다.

<Route path="/teams" element={<Teams />}>
  <Route path=":teamId" element={<Team />} />
  <Route path="new" element={<NewTeamForm />} />
  <Route index element={<LeagueStandings />} />
</Route>

이 구성에서 /teams 경로는 부모 경로가 되고, :teamId와 new는 그 아래의 자식 경로입니다. 그리고 index prop을 가진 Route 컴포넌트는 index 라우트로, 부모 경로가 일치하면 자동으로 렌더링됩니다.

따라서 URL이 /teams인 경우, 부모 라우트가 일치하지만 자식 라우트가 없으므로 index 라우트인 LeagueStandings가 렌더링됩니다. 이는 부모 경로의 기본 UI를 채우는 역할을 합니다.

index 라우트는 일반적으로 부모 경로의 기본 UI를 제공하기 위해 사용됩니다. 부모 경로가 있는 경우 자식 경로가 선택되지 않았을 때 index 라우트는 부모의 Outlet으로 렌더링 됩니다. 기본적인 화면을 보여주는 데 유용합니다.

children

중첩된 URL을 사용할 경우 children를 사용하면 됩니다.

  • about

  • about/:company

  • about/:company/:region

about의 자식은 :company이고 :company의 자식은 :region이다.

caseSensitive

caseSensitive는 React Router v6에서 제공되는 컴포넌트의 prop 중 하나입니다. 이 prop을 사용하여 경로의 대소문자 일치 여부를 지정할 수 있습니다.

일반적으로 URL은 대소문자를 구분하지 않습니다. 즉, "Well"과 "well"은 동일한 경로로 간주됩니다. 그러나 경우에 따라 대소문자를 엄격하게 일치시키고 싶은 경우가 있을 수 있습니다. 이 때 caseSensitive prop을 사용할 수 있습니다.

<Route caseSensitive path="/wEll-aCtuA11y" />

위의 코드에서는 caseSensitive prop이 true로 설정되어 있으므로, 경로의 대소문자가 정확히 일치해야 합니다. 따라서 "/wEll-aCtuA11y"와 같은 대소문자가 정확히 일치하는 경로에만 일치할 것입니다. 이 경우 "/wEll-aCtuA11y"와 같은 경로는 일치할 것이지만, "well-actua11y"와 같은 경로는 일치하지 않을 것입니다.

이렇게 caseSensitive prop을 사용하면 경로의 대소문자 일치 여부를 조절할 수 있습니다.

loader

loader는 route가 렌더링되기 전에 호출되는 함수입니다. 그래서 렌더링 되기전에 데이터를 불러오고, 데이터가 준비되면 렌더링을 시킵니다.
이 함수는 비동기 작업을 수행하여 route에 필요한 데이터를 로드할 수 있습니다.
함수는 loader prop으로 전달되는 객체를 받으며, 이 객체에는 라우트 경로에서 추출된 파라미터들이 포함됩니다.
일반적으로 데이터를 불러오는 비동기 작업을 수행하고, 그 결과를 반환합니다.
useLoaderData 훅을 통해 라우트 컴포넌트에서 로드된 데이터를 사용할 수 있습니다.

<Route
  path="/teams/:teamId"
  loader={({ params }) => {
    return fetchTeam(params.teamId);
  }}
/>;

function Team() {
  let team = useLoaderData();
  // ...
}

이렇게 비동기 함수를 사용하는 방식도 있다.

loader: () => {
  return fetch("/getSomething"); 
}

loader 함수의 역할은 주어진 경로에 대한 데이터를 로드하는 것입니다. React Router의 라우트가 렌더링되기 전에 loader 함수가 호출되어 비동기 작업을 수행하고 필요한 데이터를 가져옵니다. 이로써 라우트 컴포넌트가 렌더링되기 전에 필요한 데이터가 준비되어 있게 됩니다.

주요 역할은:

1. 비동기 데이터 로딩: loader 함수를 사용하여 비동기 작업을 수행하여 데이터를 가져옵니다. 주로 AJAX 요청이나 다른 비동기 방식을 사용하여 서버에서 데이터를 가져옵니다.

2. 라우트 파라미터 활용: loader 함수는 라우트의 동적 세그먼트에서 파싱된 파라미터를 받아와서 사용할 수 있습니다. 이를 통해 로드해야 할 데이터를 식별하고 가져올 수 있습니다.

3. 데이터 처리: loader 함수에서 받아온 데이터를 처리하고, 필요한 형식으로 가공하여 라우트 컴포넌트에 전달합니다.

4. 로딩 상태 처리: 데이터가 로드되는 동안 사용자에게 로딩 상태를 보여주는 데 사용될 수 있습니다. 일반적으로 로딩 중인 상태를 표시하는 UI를 렌더링하거나, 로딩 중인 상태를 관리하는 데 사용됩니다.

action

action은 form이나 fetcher 등에서 라우트로 전송된 제출(submission)을 처리하는데 사용됩니다. 이 함수는 route에 요청된 데이터를 받아서 필요한 작업을 수행합니다.
action 함수는 request 객체를 매개변수로 받으며, 이 객체는 route로부터 전송된 요청을 나타냅니다.
일반적으로 request 객체를 사용하여 폼 데이터를 추출하고, 해당 데이터를 기반으로 필요한 작업을 수행합니다.

<Route
  path="/teams/:teamId"
  action={({ request }) => {
    const formData = await request.formData();
    return updateTeam(formData);
  }}
/>

위의 예제에서는 /teams/:teamId 경로로 전송된 폼 데이터를 업데이트하는 비동기 함수를 정의하고 있습니다.

errorElement/ErrorBoundary

route가 렌더링 중에 예외를 throw할 때, 또는 로더나 액션 실행 중에 예외가 발생할 때, 이상한 경로로 이동했을 때 해당 예외에 대한 대체 React element 또는 component를 지정할 수 있습니다. React Element를 직접 생성하려면 errorElement를 사용하세요.

<Route
  path="/for-sale"
  // if this throws an error while rendering
  element={<Properties />}
  // or this while loading properties
  loader={() => loadProperties()}
  // or this while creating a property
  action={async ({ request }) =>
    createProperty(await request.formData())
  }
  // then this element will render
  errorElement={<ErrorBoundary />}
/>

ErrorBoundary를 사용하면 React Router가 React Element를 생성합니다.

<Route
  path="/for-sale"
  Component={Properties}
  loader={() => loadProperties()}
  action={async ({ request }) =>
    createProperty(await request.formData())
  }
  ErrorBoundary={ErrorBoundary}
/>

프로젝트 적용

// Home.tsx
import { Outlet } from "react-router-dom";
import Header from "../components/Header";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { YoutubeApiProvider } from "../context/YoutubeApiContext";

const queryClient = new QueryClient();
export default function Home() {
  return (
    <>
      <Header />
      <YoutubeApiProvider>
        <QueryClientProvider client={queryClient}>
          <div className="max-w-screen-2xl m-auto">
            <Outlet />
          </div>
        </QueryClientProvider>
      </YoutubeApiProvider >
    </>
  )
}
// App.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom"
import Home from "./pages/Home"
import NotFound from "./pages/NotFound"
import Videos from "./pages/Videos"
import VideoDetail from "./pages/VideoDetail"

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
    errorElement: <NotFound />,
    children: [
      { index: true, element: <Videos /> },
      { path: 'videos', element: <Videos /> },
      { path: 'videos/:keyword', element: <Videos /> },
      { path: 'videos/watch/:videoId', element: <VideoDetail /> }
    ]
  }
])

function App() {

  return (
    <RouterProvider router={router} />
  )
}

export default App

출처

0개의 댓글