All'alba vincerò

At dawn, I will win!

React

[React] React Router 라이브러리(1) : 싱글 페이지 애플리케이션(SPA) 구현

나디아 Nadia 2024. 8. 20. 09:36

📌 React Router 라이브러리

  • 프로덕션용으로 사용 가능한 싱글 페이지 앱을 구현(SPA)하기 위한 React Router 라이브러리
  • URL이 변경돼도 싱글페이지로 렌더링 하는 기능을 지원

  • React
    • 컴포넌트 안에 렌더링 로직 + 사이드 이펙트 로직 = 코드 복잡, 읽기 어려움, 관리 어려움
  • React Router (v6.4+ data API : Remix Framework)
    • 컴포넌트는 순수하게 렌더링 로직 관리
    • loader 함수는 사이드 이펙트 (서버 요청, 응답 코드 처리)

 

 

⏩ 설치

  • react-router-dom 패키지 설치
pnpm add react-router-dom

 


 

☑️ 라우터 생성 & 공급

⏩ <RouterProvider> 컴포넌트 (라우터 공급)

<RouterProvider>

import { StrictMode } from 'react';
import { RouterProvider } from 'react-router-dom';
import router from '@/routes/router';

function App() {
  return (
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
}

export default App;

 

 

 

 

createBrowserRouter()

: 라우터(router) 생성

createBrowserRouter()

  • 루트(route) 별 페이지 컴포넌트 설정
  • 6.4 버전 이상에서 사용
  • routes 필수(배열)
  • route (객체)

 

1. 불러오기

import { createBrowserRouter } from 'react-router-dom';

 

 

2. routes 선언

  • 어떤 경로에 어떤 컴포넌트를 렌더링 할 것인지 설정
  • path = 경로
  • element = 요소
const routes = [
  { path?: string, element?: React.ReactNode | null }
]



3. router 선언

const router = createBrowserRouter(routes);

 

import { createBrowserRouter } from 'react-router-dom';

import RootLayout from '@/pages/layout/RootLayout';
import HomePage from '@/pages/Home';

import NotesLayout from '@/pages/layout/NotesLayout';
import NoteListPage from '@/pages/NoteList';
import NewNotePage from '@/pages/NewNote';
import NoteDetailPage from '@/pages/NoteDetail';

const routes = [
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: 'notes',
        element: <NotesLayout />,
        children: [
          {
            index: true,
            element: <NoteListPage />,
          },
          {
            path: 'new',
            element: <NewNotePage />,
          },
          {
            path: 'detail',
            element: <NoteDetailPage />,
          },
        ],
      },
    ],
  },
];

const router = createBrowserRouter(routes);

export default router;

 

 

 

 

createRoutesFromElements()

: 라우터(router) 생성

createRoutesFromElements()

  • JSX 구문을 사용하여 루트 설정
  • (<Route>) 컴포넌트 활용
    - React Elements를 사용해서 Routes를 생성
  • 6.4 버전 이하에서 사용(Legacy)
const routesFromElement = createRoutesFromElements(
  <>
    <Route path="/" element={<HomePage />} />
    <Route path="/notes" element={<NoteListPage />} />
    <Route path="/notes/new" element={<NewNotePage />} />
    <Route path="/notes/detail" element={<NoteDetailPage />} />
  </>
);

const router = createBrowserRouter(routesFromElement);

 

 

 

 

 fallbackElement 속성

: 데이터를 가져오는 동안 UI에 표시할 대체 요소를 지정하는 데 사용

  • 앱이 서버 렌더링을 하지 않는 경우, createBrowserRouter는 마운트 할 때 일치하는 모든 route loader를 시도한다.
    ➡︎ 이 시간동안 사용자에게 앱이 작동 중이라는 표시를 제공하기 위해 fallbackElement를 제공할 수 있다.
  • 주로 createBrowserRouter 또는 createHashRouter와 함께 사용하는 라우트 설정에서 사용한다.
  • 특징
    • 비동기 로딩에 최적화
      : fallbackElement는 로드 시간이 긴 데이터에 대해 사용자 경험을 개선하는 데 유용하다.
    • 라우트별로 설정 가능
      : 각 라우트에 대해 개별적으로 fallbackElement를 지정할 수 있어, 상황에 맞는 로딩 화면을 제공할 수 있다.
import { createBrowserRouter } from 'react-router-dom';

import RootLayout from '@/pages/layout/RootLayout';
import HomePage from '@/pages/Home';
import NotesLayout from '@/pages/layout/NotesLayout';
import NoteListPage from '@/pages/NoteList';
import NewNotePage from '@/pages/NewNote';
import NoteDetailPage from '@/pages/NoteDetail';
import LoadingSpinner from '@/components/LoadingSpinner'; // 로딩 스피너 컴포넌트 추가

const routes = [
  {
    path: '/',
    element: <RootLayout />,
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: 'notes',
        element: <NotesLayout />,
        fallbackElement: <LoadingSpinner />, // 로딩 중 표시될 요소 추가
        children: [
          {
            index: true,
            element: <NoteListPage />,
            fallbackElement: <LoadingSpinner />, // 로딩 중 표시될 요소 추가
          },
          {
            path: 'new',
            element: <NewNotePage />,
            fallbackElement: <LoadingSpinner />, // 로딩 중 표시될 요소 추가
          },
          {
            path: 'detail',
            element: <NoteDetailPage />,
            fallbackElement: <LoadingSpinner />, // 로딩 중 표시될 요소 추가
          },
        ],
      },
    ],
    fallbackElement: <LoadingSpinner />, // 최상위 경로에 로딩 중 표시될 요소 추가
  },
];

const router = createBrowserRouter(routes);

export default router;

 

fallbackElement 속성

 


 

☑️ 페이지 내비게이션

<Link> 컴포넌트

: 다른 페이지로 이동 시 리-로드(Re-load) 안하는 방법

  • <Link>를 통해 이동할 때, 애플리케이션은 서버에서 새로운 HTML을 가져오는 대신,
    클라이언트에서 JavaScript를 통해 뷰를 변경하고 상태를 업데이트합니다.
  • <a href="/"> 대신 <Link> 사용
  • to 속성 필수
<Link to={/path} ></Link>

<Link>

 

 

 

 

<NavLink> 컴포넌트

: 네비게이션 용 링크 컴포넌트

  • 일반 <Link>와 달라짐
    - 활성 링크(active link) 표시
    - aria-current 속성, class="active" 속성 들이 추가됨
<NavLink to={/path} ></NavLink>

 

 

  • end 속성
    : 선택된 네비게이션 메뉴만 활성화 하는 속성
    • 경로가 정확히 일치해야 활성화 상태로 간주
<NavLink to={/path} end ></NavLink>

 

  • 특정 컴포넌트의 하위 컴포넌트도 네비게이션 메뉴에 있을 경우,
    경로가 "정확히 일치"할 때만 링크가 활성화되도록 로직을 추가해야 한다.
    ex) '/home' 경로에 있는 경우, '/home'은 활성화되지만, '/home/settings'는 활성화되지 않는다.
let end = false;

if (item.path?.endsWith('/') || item.path === '/notes') {
  end = true;
}

 

 

<NavLink 
   to={/path} 
   end
   className={({ isActive }) => {
     return isActive ? 'a-active' : undefined;
   }} 
 >
</NavLink>

<NavLink>

 


 

☑️ <RootLayout> 컴포넌트

  • 공통 레이아웃 요소(헤더, 푸터, 전역내비게이션 등) 포함하는 컴포넌트
    - 어느 페이지에도 포함되는 공통 요소
    - 중첩된 루트를 포함하는 상위 컴포넌트

 

 

  • routes에 <Root Layout> 컴포넌트를 넣고 그 하위 컴포넌트들을 children[]으로 넣는다.
  • childeren은 배열[]
  • 인덱스(index) ➡︎ 루트 설정
    - 해당 컴포넌트의 인덱스를 index: true로 설정하면 해당 컴포넌트를 루트(root, 홈)으로 보여진다.
  • 절대(absolute) 경로 vs. 상대(relative) 경로 설정 (참고)
    - 절대 경로: /notes/new, 상대 경로: new
const routes = [
  // Root Layout (Parent)
  {
    path: '/',
    element: <RootLayout />,

    // Nested Routes
    // Children components
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: 'notes',
        element: <NotesLayout />,
        children: [
          {
            index: true,
            element: <NoteListPage />,
          },
          {
            path: 'new',
            element: <NewNotePage />,
          },
          {
            path: 'detail',
            element: <NoteDetailPage />,
          },
        ],
      },
    ],
  },
];

<RootLayout>

 

 


 

☑️ 중첩 루트

⏩ <Outlet> 컴포넌트

: 중첩된 루트(Nested routes) 의 자식 컴포넌트를 렌더링할 위치를 지정

  • <Outlet> 컴포넌트를 사용해야 중첩된 루트(하위 컴포넌트)의 경로를 사용할 수 있음 (출구)
    ➡︎ <Outlet>이 없으면 중첩에서 나올 수 없음
  • 보통 라우트 계층 구조의 부모 컴포넌트에서 한 번만 사용한다.
const routes = [
  // Root Layout (Parent)
  {
    path: '/',
    element: <RootLayout />,

    <!-- Children components -->
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: 'notes',
        element: <NotesLayout />,
        children: [
          {
            index: true,
            element: <NoteListPage />,
          },
          {
            path: 'new',
            element: <NewNotePage />,
          },
          {
            path: 'detail',
            element: <NoteDetailPage />,
          },
        ],
      },
    ],
  },
];
function RootLayout() {
  return (
    <>
      <Header />
      <main>
        <Outlet />
      </main>
      <Footer />
    </>
  );
}

<Outlet>

 

 

 

 

⏩ useOutletContext()

: 중첩된 라우트 구조에서 부모 라우트가 자식 라우트에 데이터를 전달할 때 사용

  • 중첩된 루트 컴포넌트가 부모 라우트 컴포넌트로부터 전달된 컨텍스트(context)를 읽어올 수 있다.
  • 사용
    • 부모 컴포넌트 👉 ` <Outlet context={...} />`을 통해 자식에게 데이터를 전달
    • 자식 컴포넌트 👉 `useOutletContext()`로 해당 데이터를 읽어옴

 

  • 부모 컴포넌트에서 컨텍스트 제공
import { Outlet } from 'react-router-dom';

function ParentComponent() {
  const contextValue = { user: 'Jane Doe', role: 'admin' };

  return (
    <div>
      <h1>Parent Component</h1>
      <Outlet context={contextValue} /> <!-- !!! -->
    </div>
  );
}

 

  • 자식 컴포넌트에서 컨텍스트 사용
import { useOutletContext } from 'react-router-dom';

function ChildComponent() {
  const context = useOutletContext(); // !!! 

  return (
    <div>
      <h2>Child Component</h2>
      <p>User: {context.user}</p> <!-- !!! --> 
      <p>Role: {context.role}</p> <!-- !!! -->
    </div>
  );
}

 

  • 라우트 설정
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ParentComponent from './ParentComponent';
import ChildComponent from './ChildComponent';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<ParentComponent />}>
          <Route path="child" element={<ChildComponent />} />
        </Route>
      </Routes>
    </Router>
  );
}

export default App;

<useOutletContext> 

 

 


 

☑️ 에러 처리 

 <NotFound> 컴포넌트

<NotFound>

  • errorElement 속성: 특정 라우트에서 발생한 에러를 처리하기 위한 컴포넌트를 정의할 때 사용

 

1. NotFound 컴포넌트(에러 발생 시 보여줄 오류 페이지(404)) 만들기

// NotFound.js
import React from 'react';

function NotFound() {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
      <p>The page you are looking for does not exist.</p>
    </div>
  );
}

export default NotFound;

 

 

2. router에 연결하기

// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ParentComponent from './ParentComponent';
import ChildComponent from './ChildComponent';
import NotFound from './NotFound'; // NotFound 컴포넌트를 import

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<ParentComponent />}>
          <Route path="child" element={<ChildComponent />} />
        </Route>
        {/* 잘못된 경로에 대해 NotFound를 렌더링 */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}

export default App;

 

 

 

 

 useRouteError()

: 에러를 만들 수 있음

  • React Router에서 불러옴

 

1. 불러오기

import { useRouteError } from 'react-router-dom';

 

 

2. useRouteError 선언

  const { status, statusText, error } = useRouteError();
 // useRouteError 객체에서 구조 분해 할당

 

 

3. 사용

  return (
    <>
      <Header />
      <main role="alert">
        <h1>
          {status} {statusText} 오류 발생
        </h1>
        <p>{error.message}</p>
      </main>
      <Footer />
    </>
  );

 

import { useRouteError } from 'react-router-dom';
import Footer from './layout/Footer';
import Header from './layout/Header';

function ErrorPage() {
  const { status, statusText, error } = useRouteError();

  return (
    <>
      <Header />
      <main role="alert">
        <h1>
          {status} {statusText} 오류 발생
        </h1>
        <p>{error.message}</p>
      </main>
      <Footer />
    </>
  );
}

export default ErrorPage;

 

 


 

 

Home v6.26.1 | React Router

 

reactrouter.com