라우팅은 사용자가 요청한 URL을 분석해 그 요청에 맞는 자원을 반환하거나 특정 동작을 수행하도록 연결하는 과정이다. 주로 웹 애플리케이션에서 클라이언트가 특정 URL로 접근하면 서버나 프론트엔드가 이에 대한 응답을 처리하기 위해 라우팅을 설정한다.
❗️ URL 뒤의 엔드 포인트에 따라 각 페이지를 불러온다.
SPA(Single Page Application)는 단일 HTML 페이지로 구성된 웹 애플리케이션이다. 페이지 이동 없이 브라우저에서 동적으로 콘텐츠를 갱신하며 사용자 경험을 개선한다.
이 패키지를 프로젝트에 설치함으로써 우리는 URL 변경을 감지하고 다양한 콘텐츠를 로딩할 수 있게 된다.
npm install react-router-dom
URL과 경로에 대해 어떤 컴포넌트가 로딩되어야 하는지 정의한다.
예를 들어 https://naver.com/pages 라는 URL 이 있을 때 naver.com이 도메인이고, /pages 부분을 경로라고 한다. (path)
import { createBrowserRouter, RouterProvider } from "react";
const router = createBrowserRouter([ // 라우터 생성
{ path: "/", element: <HomePage /> }, // 경로 & 경로에 따른 컴포넌트 지정
{ path: "/products", element: <ProductsPage /> },
]);
function App() {
return <RouterProvider router={router} />; // 라우터 객체 연결 & 반환
}
라우터를 활성화하고 라우트 정의를 로딩한다.
로딩하려는 컴포넌트들이 있는지 확인하고 그 페이지들 간에 이동할 수단을 제공했는지 확인한다.
URL의 path 를 변경하면 그 경로에 지정된 페이지로 바뀌도록 위에서 Router를 설정해주었다.
그렇다면 사용자들은 이 path를 어떻게 이용하여 페이지를 이동할까?
❌ 기존 방식
<p>
Go to <a href="/products">the products pages</a>
</p>
위와 같이 a 태그를 사용한 페이지 이동 방식은 URL을 변경하지만, 페이지 자체를 새로고침하기 때문에 SPA 장점을 활용하지 못한다.
그 대신 react-router-dom 에서 지원하는 Link & to 를 사용하면 페이지의 새로고침 없이 URL의 path만 변경하여 페이지를 전환할 수 있다.
➡️ 새로운 HTTP 요청을 보내지 않고 클라이언트 사이드에서 페이지를 렌더링하는 것이다!
✅ SPA 방식
<p>
Go to <Link to="/products">the products pages</Link>
</p>
// 빠르고 부드러운 페이지 전환을 가능하게 한다.
react-router-dom 같은 라이브러리를 사용해 라우터를 설정해야 한다.➡️ 라우터에서 페이지를 SPA로 지정한다고만 할 수 없고 라우터 설정 + 클라이언트 사이드 렌더링 이 함께 동작해야 SPA이다.
위에서 말한 a 태그는 이러한 구조와 동작에 맞지 않으므로 SPA의 장점을 활용하지 못한다는 것이다.
Outlet은 React Router에서 중첩된 라우트를 렌더링하는 데 사용되는 특별한 컴포넌트이다. 부모 라우트(최상위 라우트)가 정의되어 있는 컴포넌트 내부에서 자식 라우트를 렌더링할 위치를 지정할 때 사용한다.
공통 레이아웃
네비게이션 바, 푸터, 사이드 바처럼 모든 페이지에서 공통적으로 보여야 하는 요소가 있을 때 사용한다.
중첩된 라우트
특정 경로 아래에 또 다른 라우트를 중첩해 구조화하고 싶을 때 유용하다.
ex)/products 아래 /products/details 와 같은 하위 경로를 관리하고자 할 때 사용
Outlet을 통해 자식 라우트가 렌더링될 위치를 제공한다.Outlet에 렌더링되며, 부모의 요소와 함께 표시된다.➡️ 부모와 자식 라우트가 분리된 역할을 가지면서도 하나의 레이아웃을 공유할 수 있도록 돕는다.
📁 App.js
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />, // 최상위 라우트
children: [ // 중첩되는 자식 라우트
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
],
},
]);
최상위 레이아웃 컴포넌트에서 Outlet을 추가해 자식 라우트가 렌더링될 위치를 지정한다.
📁 Root.js
import { Outlet } from "react-router-dom";
function RootLayout() {
return (
<>
<h1>Root Layout</h1>
<Outlet /> // 자식 라우트 렌더링 될 자리
</>
);
}
export default RootLayout;
공통으로 사용할 최상위 레이아웃 컴포넌트에 들어갈 네비게이션 바를 따로 작성한다.
📁 MainNavigation.js
import { Link } from "react-router-dom";
function MainNavigation() {
return (
<header>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/products">Products</Link>
</li>
</ul>
</header>
);
}
export default MainNavigation;
공통적으로 보여야 하는 네비게이션 바를 RootLayout에 추가한다.
📁 Root.js
import { Outlet } from "react-router-dom";
import MainNavigation from "../components/MainNavigation";
function RootLayout() {
return (
<>
<MainNavigation />
<Outlet />
</>
);
}
export default RootLayout;
💡 결론
Outlet은 SPA에서 공통 UI를 유지하며 각 페이지를 동적으로 렌더링할 수 있게 해주는 핵심적인 도구이다.
부모 라우트와 자식 라우트 간의 역할을 명확히 구분한다.
errorElement는 React Router에서 특정 라우트나 해당 라우트의 하위 라우트에서 에러가 발생했을 때 렌더링할 컴포넌트를 지정하는 옵션이다.
ex) 잘못된 엔드 포인트로 URL에 접속했을 때 띄우는 페이지
📁 error.js
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />, // 에러 발생 시 렌더링될 컴포넌트
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
],
},
import MainNavigation from "../components/MainNavigation";
export default function ErrorPage() {
return (
<>
<MainNavigation /> // 네비게이션 (부모 컴포넌트 - Outlet)
<main>
<h1>An error occurred!</h1>
<p>Could not find this page</p>
</main>
</>
);
}
Link 를 써서 페이지를 이동하는 방법은 css 를 설정한다면, 클릭하거나 마우스를 가져다놓았을 때의 디자인은 쉽게 할 수 있다. 하지만 현재 페이지에 접속해있을 동안 그 페이지 인 것을 알 수 있게 할 방법이 없다.
이때 NavLink는 React Router에서 제공하는 컴포넌트로, 네비게이션에서 현재 활성화된 링크를 강조하거나 스타일을 적용할 때 사용된다. 기본적인 페이지 이동은 Link로 처리할 수 있지만, NavLink는 활성 상태를 감지해 네비게이션 UI에서 현재 페이지를 사용자에게 표시할 수 있는 기능을 제공한다.
❗️
NavLink는to속성에 지정된 경로가 현재 URL과 매칭되면 기본적으로active라는 클래스가 자동으로 추가된다.
NavLink 는 현재 URL 경로와 to 속성 값을 비교해 활성 상태를 결정한다.active 클래스가 적용된다.className이나 style 속성을 콜백으로 설정하면 isActive 값을 활용해 커스텀 동작을 구현할 수 있다.👀 활성화된 경우
active클래스가 적용된다는 것이 무슨 말일까?
active 클래스가 적용된다는 것은, React Router의 NavLink 컴포넌트가 현재 페이지 URL과 to 속성에 설정된 경로가 일치하면, 해당 <NavLink> 엘리먼트에 자동으로 active라는 CSS 클래스가 추가된다는 의미이다. 이 클래스는 개발자가 스타일링할 때 활용할 수 있다.
예를 들면,
<nav> // 기본 네비게이션 링크
<NavLink to="/" end>Home</NavLink>
<NavLink to="/products">Products</NavLink>
</nav>
<nav> // 현재 URL이 /이면
<a href="/" class="active">Home</a> // active 클래스 추가됨 --> a.active로 사용
<a href="/products">Products</a>
</nav>
👀 경로가
/인 경우, 모든 링크가/로 시작되어 활성화 되는데 어떻게 해야할까?
NavLink의 기본 동작은 URL 경로가 to 속성에 지정된 값과 부분적으로만 일치해도 active 클래스를 추가하는 것이다.
따라서 아래와 같은 상황에서 /products 로 끝나는 페이지에 접속을 하면 두 링크가 모두 활성화가 되는 것이다.
이런 경우 NavLink에서 지원하는 end 속성을 이용하여 이 path가 마지막에 오는지를 확인하면 된다.
예를 들어 / 는 뒤에 다른 경로가 더 올 수 있으므로 end=true인지를 확인해야 한다.
(/products 는 뒤에 더 붙지 않으므로 확인할 필요 없음)
import { NavLink } from "react-router-dom";
import classes from "./MainNavigation.module.css";
<NavLink
to="/"
className={({ isActive }) =>
isActive ? classes.active : undefined
}
end
>
Home
</NavLink>
<NavLink
to="/products"
className={({ isActive }) =>
isActive ? classes.active : undefined
}
>
Products
</NavLink>
useNavigate는 React Router에서 제공하는 훅으로, 프로그래밍 방식으로 페이지 이동을 수행할 수 있게 해준다. 사용자가 버튼을 클릭하거나 특정 이벤트가 발생했을 때 코드로 경로를 변경하는 데 유용하다.
useNavigate 는 라우터에서 제공되는 내장 함수 navigate 를 반환한다.to 속성으로 지정한 경로로 이동하며, 새로고침 없이 URL과 UI를 반환한다.navigate("/path")navigate(-1)navigate(1)const navigate = useNavigate();
지정한 경로로 사용자를 이동시킨다.
function navigateHandler() {
navigate("/products");
}
사용자가 버튼을 클릭하면 navigateHandler 함수가 실행되고, UI가 /products 페이지로 변경된다.
<button onClick={navigateHandler}>Navigate</button>
import { Link, useNavigate } from "react-router-dom";
function HomePage() {
const navigate = useNavigate();
function navigateHandler() {
navigate("/products");
}
<button onClick={navigateHandler}>Navigate</button>
useParams는 React Router DOM에서 제공하는 훅으로, URL 경로에 정의된 동적 경로 매개변수를 읽어오는 데 사용된다. 동적 경로를 기반으로 특정 데이터를 렌더링하거나 조건에 따라 UI를 변경할 수 있다.
/products/:productId, 사용자 프로필 페이/users/:userId)동적 경로는 콜론(:)을 사용해 정의한다. 예를 들어, /products/:productId는 productId를 동적으로 처리하도록 설정한 경로이다.
(이 :productId는 특정 값(p1, p2 등)에 대응된다.)
📁 App.js
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
{ path: "/products/:productId", element: <ProductDetailPage /> }, // 뒤에 오는 path 가 동적임을 react-router-dom에게 알려줌 :/productId 로!
],
},
]);
Link를 사용해 동적으로 생성된 경로로 이동할 수 있는 링크를 만든다. 경로에 포함된 :productId 는 데이터에서 동적으로 설정된다.
📁 Product.js
const PRODUCTS = [ // 백엔드에서 받아올 수도 있음
{ id: "p1", title: "Product 1" },
{ id: "p2", title: "Product 2" },
{ id: "p3", title: "Product 3" },
];
<ul>
{PRODUCTS.map((product) => (
<li key={product.id}> // 경로로 이동
<Link to={`/products/${product.id}`}>{product.title}</Link>
</li>
))}
</ul>
useParams를 이용해 URL의 동적 경로에 포함된 값을 읽는다. useParams는 URL에 지정된 매개변수의 키와 값을 객체 형태로 반환한다.
❗️ 값을 읽어올 때는 라우터에서 정의한 식별자 이름을 그대로 써야 한다!!
path="/products/:productId"로 정의했다면, params.productId로 값을 가져와야 한다.
이름이 일치하지 않으면 값을 읽을 수 없다.
❗️ 매개변수 값은 문자열로 반환된다.
숫자를 사용해야 할 경우 parseInt 같은 변환이 필요하다.
📁 ProductDetail.js
import { useParams } from "react-router-dom";
export default function ProductDetailPage() {
const params = useParams(); // { productId: 'p1' }
return (
<> // 라우터에서 정의한 식별자 그대로 써야함!!
<h1>{params.productId} Product Details!</h1>
</>
);
}
💡 요약
NavLink 사용 (현재 경로 강조 필요)Link 사용useNavigate 사용 (버튼 클릭, 조건부 이동 등)📁 app.js
const router = createBrowserRouter([
{
path: "/root", // 부모
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "", element: <HomePage /> }, // 자식 1
{ path: "products", element: <ProductsPage /> }, // 자식 2
{ path: "products/:productId", element: <ProductDetailPage /> }, // 자식 3
],
},
]);
구문: /products/ 처럼 슬래시(/)로 시작하는 경로.
특징:
❗️ 절대 경로는 도메인 이름 바로 뒤에 추가되고, 현재 활성인 경로 뒤에 추가되지 않는다!
<Link to={`/products/${product.id}`}>{product.title}</Link>
✅ /root/home ➡️ /products/${product.id}
따라서 이전 페이지로 가고싶을 때, 아래와 같이 ..로 바로 전 페이지로 이동 가능하다.
ex) /root/products/p1 → .. → /root/products/
📁 Product.js
<Link to={`/products/${product.id}`}>{product.title}</Link>
📁 ProductDetail.js
<Link to="..">Back</Link> // 한 단계 이전으로
products/처럼 슬래시(/) 없이 작성한 경로. <Link to={product.id}>{product.title}</Link>
✅ /root/products/ ➡️ /root/products/${product.id}
상대 경로는 이전 페이지로 가고싶을 때, 아래와 같이 ..를 하면 상위 경로로 돌아간다.
건너온 경로들을 무시하고 아예 라우터에 지정한 부모 경로로 돌아가는 것이다.
ex) /root/products/p1 → .. → /root
이 문제점을 막기 위해 상대 경로에는 relative='path' 라는 속성이 있다.
이것을 지정하면 한단계 이전 경로로도 돌아갈 수 있다.
(지정 안하면 relative='route' : 이게 기본 값)
📁 Product.js
<Link to={product.id}>{product.title}</Link>
📁 ProductDetail.js
<Link to="..">Back</Link> // 한 단계 이전으로
상대 경로 vs 절대 경로
| 경로 유형 | 절대 경로 | 상대 경로 |
|---|---|---|
| 작성 방식 | /products/ | products/ |
| 이동 기준 | 도메인 이름 뒤에 바로 추가됨 | 현재 활성 경로 뒤에 경로가 추가됨 |
| 사용 사례 | 명확한 경로로 이동이 필요할 때 | 현재 위치를 기준으로 경로를 추가할 때 |
..(상위) | 한단계 이전으로 이동 | - 기본 : 부모 라우터로 이동 |
| - relative="path" 속성 지정 시 한 단계 이전으로 이동 가능 |
👀 그럼 언제 절대 경로를 사용하고 언제 상대 경로를 사용할까?
절대 경로 - 간단한 네비게이션 메뉴상대 경로 - 동적 데이터 기반 작업이나 중첩 라우트index: true는 기본적으로 부모 경로의 기본 페이지(Default Page)를 지정할 때 사용하는 옵션이다.
루트 또는 특정 부모 경로에서 별도의 경로를 지정하지 않아도 자동으로 렌더링되는 페이지를 설정할 수 있다.
아래와 같이 기본 페이지에서는 경로가 없어 path: "" 라고 지정되는 경우에 index: true 속성을 사용하여 루트 페이지가 기본 페이지라고 알려준다.
이렇게 작성하면 기본 페이지가 어디인지 명확히 알 수 있으므로 권장되는 방법이다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
// { path: "", element: <HomePage /> },
✅ { index: true, element: <HomePage /> }, // path: ""
{ path: "products", element: <ProductsPage /> },
{ path: "products/:productId", element: <ProductDetailPage /> }, // 뒤에 오는 path 가 동적임을 react-router-dom에게 알려줌 :/productId 로!
],
},
]);
⭐️ 코드 꼭 외워두기 !! ⭐️
맨날 헷갈리는 부분
1. useEffect
2. response.json()
3. async & await
4. await response.json()
5. 함수 호출
6. []
const [isLoading, setIsLoading] = useState(false);
const [fetchedEvents, setFetchedEvents] = useState();
const [error, setError] = useState();
useEffect(() => {
async function fetchEvent() {
setIsLoading(true);
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
setError("Fetching events failed");
} else {
const resData = await response.json();
setFetchedEvents(resData.events);
}
setIsLoading(false);
}
fetchEvent();
}, []);
해당 라우터가 렌더링 되기 전에 loader에 있는 함수가 먼저 실행되는 것
React Router의 loader는 특정 경로를 렌더링하기 전에 데이터를 미리 가져오는 데 사용된다.
라우터가 렌더링되기 전에 loader가 실행되며, 이 데이터를 컴포넌트에서 사용 가능하다.
loader는 async 함수로 작성 가능하며, promise를 반환해 비동기 데이터를 가져온다.loader에서 에러가 발생하면 해당 라우트의 errorElement가 표시된다.Context 전달useLoaderData()를 사용하여 loader가 반환한 데이터를 컴포넌트에서 사용할 수 있다.{ path: "/", element: <EventsPage />, loader: () => {} }
사용자가 라우트에 진입할 때
ex) 사용자가 /events 경로로 직접 이동하거나, 링크를 클릭해 해당 경로로 이동할 때
라우트 간 탐색 시
다른 라우트에서 이동하며 새로운 라우트가 활성화될 때
프로그램적으로 탐색할 때
useNavigate 또는 navigate를 통해 경로를 변경할 때
페이지 새로고침 시
사용자가 브라우저를 새로고침하면 해당 경로와 연결된 loader가 다시 실행
EventsPage 에서 데이터를 불러오는 비동기 코드를 App.js에서 loader 안에 작성한다.
이렇게 하면 EventsPage가 렌더링되기 전에 데이터를 미리 가져올 수 있고, 그 데이터를 EventsPage에 쉽게 보낼 수 있다.
{
index: true,
element: <EventsPage />,
loader: async () => {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
//
} else {
const resData = response.json();
return resData.events;
}
},
},
가장 가까운 loader 데이터에 엑세스 하기 위해 사용한다.
위의 loader 에서 리턴한 데이터들을 받아와서 활용한다.
📁Events.jsx
import { useLoaderData } from 'react-router-dom';
function EventsPage() {
const events = useLoaderData();
return <EventsList events={events} />;
}
📁EventsList.jsx
import { useLoaderData } from 'react-router-dom';
function EventsPage() {
const events = useLoaderData();
return
<>
{events.map((event) => {...})};
</>;
}
✔️ Before
<// 컴포넌트 안에서 useEffect로 데이터를 불러와서 사용한다.
// 이렇게 되면 다른 컴포넌트에서 데이터를 사용할 때 다시 보내주어야 하므로 불편함
📁 App.js
function App() {
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventLayout />,
children: [
{ path: "", element: <EventsPage /> },
{ path: ":eventId", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":eventId/edit", element: <EditEventPage /> },
],
},
],
},
]);
return <RouterProvider router={router} />;
}
export default App;
📁 Events.js
import { useEffect, useState } from "react";
import EventsList from "../components/EventsList";
function EventsPage() {
const [isLoading, setIsLoading] = useState(false);
const [fetchedEvents, setFetchedEvents] = useState();
const [error, setError] = useState();
useEffect(() => {
async function fetchEvent() {
setIsLoading(true);
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
setError("Fetching events failed");
} else {
const resData = response.json();
setFetchedEvents(resData.events);
}
setIsLoading(false);
}
fetchEvent();
}, []);
return (
<>
<div style={{ textAlign: "center" }}>
{isLoading && <p>Loading...</p>}
{error && <p>{error}</p>}
</div>
{!isLoading && fetchedEvents && <EventsList events={fetchedEvents} />}
</>
);
}
export default EventsPage;
✔️ After
// 라우터 안에서 데이터를 불러옴
// 필요한 컴포넌트에 라우터의 loader를 통해 데이터를 각각 보내므로 쉽게 데이터 사용 가능
📁 App.js
function App() {
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: async () => {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
//
} else {
const resData = response.json();
return resData.events;
}
},
},
{ path: ":eventId", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":eventId/edit", element: <EditEventPage /> },
],
},
],
},
]);
return <RouterProvider router={router} />;
}
export default App;
📁 Events.js
import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const events = useLoaderData();
return <EventsList events={events} />;
}
export default EventsPage;
정답은 아니다.
❗️ 낮은 레벨에서 정의된 라우트에서 받아온 데이터는 그 상위 컴포넌트에서는 사용할 수 없다!!
따라서 아래 코드에서 예시를 들면 EventLayout을 루트로 하는 컴포넌트에서는 아래의 방식대로 하면 언제든지 데이터를 사용할 수 있다.
import { useLoaderData } from 'react-router-dom' const data = useLoaderData()하지만 RootLayout에 있는 Homepage 등 그 상위 컴포넌트에서는 데이터를 꺼내올 수 없다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventLayout />,
children: [
{
index: true,
element: <EventsPage />,
loader: async () => {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
//
} else {
const resData = await response.json();
return resData.events;
}
},
},
{ path: ":eventId", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":eventId/edit", element: <EditEventPage /> },
],
},
],
},
]);
하지만 loader 함수를 App.js 에서 익명함수로 사용하면 App.js가 방대해진다는 단점이 있다. 이를 위해 다른 곳에 함수를 지정하고 App.js에서 함수를 가져오는 방식을 취할 수 있다.
❗️ 필요한 컴포넌트에 더 가까운 곳에 있는 함수에 아웃소싱 해서 App.js에서 가져와 사용한다면 더 간단하게 만들 수 있다.
📁 App.js
import { loader as eventsLoader } from "./pages/EventsPage";
{
index: true,
element: <EventsPage />,
loader: eventsLoader(), // 함수 불러옴
}
📁 EventsPage.js
import { useLoaderData } from "react-router-dom";
import EventsList from "../components/EventsList";
function EventsPage() {
const events = useLoaderData();
return <EventsList events={events} />;
}
export default EventsPage;
export async function loader() { // 아웃소싱
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
//
} else {
const resData = await response.json();
return resData.events;
}
}
💡 결론
loader 는 비동기 데이터 로드와 초기화를 처리하는 도구이다.loader를 적극적으로 사용하자!!Navigation은 애플리케이션 내에서 사용자가 페이지 간 이동(라우팅)을 할 수 있도록 관리하는 시스템이다.
React Router와 같은 라이브러리는 이러한 네비게이션 기능을 제공하여 SPA(Single Page Application)에서도 부드럽고 직관적인 사용자 경험을 구현할 수 있다.
loader()를 활용하여 데이터를 로드할 수 있다.idle, loading, submitting)를 관리한다.
idle : 라우터 전환이 일어나지 않음loading : 라우터 전환이 일어나고 데이터를 로딩 중submitting : 라우터 전환이 일어나고 데이터를 제출import { useNavigation } from "react-router-dom";
function App() {
const navigation = useNavigation(); // Navigation 상태 가져오기
return (
<div>
{navigation.state <=== "idle" && <p>앱이 대기 중입니다.</p>}
{navigation.state === "loading" && <p>로딩 중입니다...</p>}
{navigation.state === "submitting" && <p>데이터를 제출 중입니다...</p>}
</div>
);
}
loader에서 에러가 나더라도 위의 errorElement를 찾을 때까지 올라오다가 상위에서 errorElement를 만나면 그 컴포넌트가 실행된다.
<function App() {
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{
path: "events",
element: <EventLayout />,
children: [
{
index: true,
element: <EventsPage />,
// errorElement: <ErrorPage />, // 만약 여기에 에러페이지 설정했다면
// loader에서 에러 발생 시 여기 컴포넌트 실행
// 하지만 여기에 설정안하고 상위에서 설정했다면
// errorElement를 찾을 때까지 거슬러 올라가다가 만나면 그 에러 컴포넌트 실행
loader: eventsLoader,
},
{ path: ":eventId", element: <EventDetailPage /> },
{ path: "new", element: <NewEventPage /> },
{ path: ":eventId/edit", element: <EditEventPage /> },
],
},
],
},
]);
useRouteError 훅은 라우팅 과정에서 발생한 에러를 처리할 수 있도록 해준다. 이를 통해 특정 경로에서 로드한 데이터가 실패하거나 예상치 못한 오류가 발생했을 때 사용자에게 유용한 피드백을 제공할 수 있다.
loader 함수는 데이터를 가져오기 전에 실행되므로, 네트워크 요청 실패나 예외 상황에서 throw를 통해 에러를 발생시킬 수 있다.
❗️ 에러는 JSON 데이터를 포함한 Response 객체로 만들어야 하며, 상태 코드 status와 메시지를 명시할 수 있다.
📁 EventsPage.js
export async function loader() {
const response = await fetch("http://localhost:8080/events");
if (!response.ok) {
throw new Response(JSON.stringify({ message: "Could not fetch event" }), {
status: 500,
});
} else {
const resData = await response.json();
return resData.events;
}
}
useRouteError 훅을 사용해 라우터가 제공하는 여러 객체를 가져온다.
에러 객체는 아래의 정보를 포함한다.
status: HTTP 상태 코드data : throw로 전달한 JSON 데이터 (이 코드에서는 message)📁 ErrorPage.js
import { useRouteError } from "react-router-dom";
import PageContent from "../components/PageContent";
export default function ErrorPage() {
const error = useRouteError();
error.status // 위에서 설정한 대로 에러난 경우 : 500
let title = "An error occurred!";
let message = "Something went wrong!";
if (error.status === 500) {
message = JSON.parse(error.data).message; // JSON 객체를 문자열로 전환
}
if (error.status === 404) {
title = "Not Found!";
message = "Could not find resource or page";
}
return (
<PageContent title={title}>
<p>{message}</p>
</PageContent>
);
}
useParams 처럼 훅은 loader 함수에서 접근할 수 없다.
📁 App.js
{
path: ":eventId",
element: <EventDetailPage />,
loader: eventDetailLoader,
},
{ path: ":eventId/edit", element: <EditEventPage /> },
위의 코드는 라우트를 중첩하기 이전의 코드이다.
이 코드에서 라우트를 중첩 시키려면 아래와 같이 작성하면 된다.
하지만 이렇게 중첩 라우트를 사용하면 loader 함수는 가장 가까운 컴포넌트에서만 사용되므로 EventDetailPage 에서는 데이터를 사용할 수 있지만, EditEventPage에서는 데이터를 사용할 수 없는 문제점이 생긴다.
📁 App.js
{
path: ":eventId",
// 공통 element 없으면 안적어도 된다.
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
},
{ path: ":eventId/edit", element: <EditEventPage /> },
],
},
따라서 아래의 코드와 같이 라우터에 id를 원하는 이름으로 지정해주고, 각 컴포넌트에서 원하는 데이터를 꺼내어 쓸 때 useRouteLoaderData('id') 를 사용한다.
이렇게 하면 중첩된 라우트의 하위에서도 데이터를 원하는대로 꺼내쓸 수 있다.
📁 App.js
{
path: ":eventId",
⭐️ id: "event-detail", ⭐️
loader: eventDetailLoader,
children: [
{
index: true,
element: <EventDetailPage />,
},
{ path: ":eventId/edit", element: <EditEventPage /> },
],
},
📁 EditEventPage.js
import { useRouteLoaderData } from 'react-router-dom';
const data = useRouteLoaderData("event-detail");
const event = data.event;
📁 EventDetailPage.js
import { useRouteLoaderData } from "react-router-dom";
const data = useRouteLoaderData("event-detail");