📌 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> 컴포넌트 (라우터 공급)
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) 생성
- 루트(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) 생성
- 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;
☑️ 페이지 내비게이션
⏩ <Link> 컴포넌트
: 다른 페이지로 이동 시 리-로드(Re-load) 안하는 방법
- <Link>를 통해 이동할 때, 애플리케이션은 서버에서 새로운 HTML을 가져오는 대신,
클라이언트에서 JavaScript를 통해 뷰를 변경하고 상태를 업데이트합니다. - <a href="/"> 대신 <Link> 사용
- to 속성 필수
<Link to={/path} ></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;
}
- class명("active")을 변경하고 싶을 때
- <NavLink>의 className에 function 넣기
- class 스타일을 지정할 수 있음
<NavLink
to={/path}
end
className={({ isActive }) => {
return isActive ? 'a-active' : undefined;
}}
>
</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 />,
},
],
},
],
},
];
☑️ 중첩 루트
⏩ <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 />
</>
);
}
⏩ 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;
☑️ 에러 처리
⏩ <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;