Type something to search...

8. React Router

React Router

공식사이트

1. 라우터 이해

1.1. 라우터란

네트워크에서 데이터를 목적지까지 연결하는 장치다. 리액트에서는 URL 주소에 따라 다른 컴포넌트를 보여주는 역할을 한다.

1.2. 라우팅이란

데이터의 최단·최적 경로를 찾는 과정이다.

라우팅 개념

1.3. MPA란

MPA(Multi Page Application) — 여러 HTML 페이지를 각각 만들어 연결하는 방식이다. 페이지 이동 시 전체 페이지가 새로 로드된다.

1.4. SPA란

SPA(Single Page Application) — HTML 파일 하나에서 컴포넌트만 교체하는 방식이다. 변경된 부분만 업데이트되므로 빠르다.

SPA 개념

핵심 개념 4가지:

개념역할
loader페이지 진입 전 데이터를 가져오는 함수
useLoaderDataloader가 반환한 데이터를 컴포넌트에서 꺼내는 훅
action폼 제출(POST)을 처리하는 함수
useActionDataaction이 반환한 결과를 컴포넌트에서 꺼내는 훅

2. 설치

개발 서버를 닫고 아래 명령어로 설치한다.

Terminal window
1
npm install react-router@7

설치 완료 후 서버를 다시 실행한다.

Terminal window
1
npm run dev

3. 기본 라우팅

3.1. 컴포넌트 작성

먼저 페이지로 사용할 컴포넌트 파일을 만든다. src 아래에 pages 폴더를 만들고 두 파일을 생성한다.

1
src/
2
└── pages/
3
├── Home.jsx
4
└── Products.jsx
  1. src/pages/Home.jsx 파일을 만들고 아래 코드를 작성한다.
src/pages/Home.jsx
1
const Home = () => {
2
return <h1>Home</h1>;
3
};
4
export default Home;
설명
1Home 컴포넌트를 화살표 함수로 정의한다
2h1 태그를 반환한다
4다른 파일에서 사용할 수 있도록 내보낸다
  1. src/pages/Products.jsx 파일을 만들고 아래 코드를 작성한다.
src/pages/Products.jsx
1
const Products = () => {
2
return <h1>Products</h1>;
3
};
4
export default Products;

.js.jsx는 기능이 동일하다. 개발자가 컴포넌트 파일임을 쉽게 구분하기 위해 .jsx 확장자를 사용한다.


3.2. createBrowserRouter — 라우트 정의

createBrowserRouter — 라우터를 생성하는 함수다. 경로(path)와 컴포넌트(element)를 배열로 등록한다.

정보

라우터 연결 3단계:

1단계: 라우터 생성 — createBrowserRouter

2단계: 라우터와 앱 연결 — RouterProvider

3단계: 경로와 컴포넌트 매핑 — path, element

  1. src/main.jsx에 아래 코드를 작성한다.
src/main.jsx
1
import React from 'react';
2
import ReactDOM from 'react-dom/client';
3
import { createBrowserRouter, RouterProvider } from 'react-router';
4
import Home from './pages/Home';
5
import Products from './pages/Products';
6
7
const router = createBrowserRouter([
8
{
9
path: '/',
10
element: <Home />,
11
},
12
{
13
path: '/products',
14
element: <Products />,
15
},
16
]);
17
18
ReactDOM.createRoot(document.getElementById('root')).render(
19
<RouterProvider router={router} />
20
);
설명
3react-router에서 두 함수를 가져온다
7createBrowserRouter에 배열을 전달해 라우터를 만든다
9path — 브라우저 주소창의 경로
10element — 해당 경로에서 렌더링할 컴포넌트. 반드시 JSX(<Home />) 형태로 작성한다
18RouterProvider로 라우터 설정을 앱 전체에 제공한다
  1. 브라우저 주소창에 /products를 입력하면 새로고침 없이 Products 컴포넌트로 바뀐다.

3.2.1. element vs Component

라우트에 컴포넌트를 지정하는 방법은 두 가지다.

속성사용 시기
elementJSX 인스턴스 (<Home />)props를 직접 전달할 때
Component컴포넌트 자체 (Home)React Router가 직접 렌더링할 때
1
// element: 반드시 JSX를 넣어야 한다
2
{ path: '/', element: <Home /> } // 올바름
3
{ path: '/', element: Home } // 잘못됨 — 함수 참조 (렌더링 안 됨)
4
5
// Component: 컴포넌트를 그대로 넣는다
6
{ path: '/', Component: Home } // 올바름
7
{ path: '/', Component: <Home /> } // 잘못됨 — JSX를 넣으면 안 됨

<a href=""> 대신 <Link to="">를 사용한다.

주의

<a> 태그는 클릭 시 전체 페이지가 새로 로드된다. SPA의 장점을 잃게 된다.

<Link>는 새로고침 없이 화면만 바꾼다.

  1. Home.jsx를 아래와 같이 수정한다.
src/pages/Home.jsx
1
import { Link } from 'react-router';
2
3
const Home = () => {
4
return (
5
<div>
6
<h1>Home</h1>
7
<Link to="/products">Products 페이지로 이동</Link>
8
</div>
9
);
10
};
11
export default Home;
설명
1react-router에서 Link를 가져온다
7to="/products" — 이동할 경로를 to 속성으로 작성한다
  1. 링크를 클릭해 화면 새로고침 없이 이동하는지 확인한다.

3.4. children + Outlet — 공통 레이아웃

네비게이션처럼 모든 페이지에서 항상 보여야 하는 UI는 레이아웃 컴포넌트에 넣고, children으로 자식 라우트를 묶는다.

<Outlet />은 자식 라우트 컴포넌트가 렌더링될 자리를 표시한다.

1단계: RootLayout 컴포넌트 만들기

src/pages/RootLayout.jsx
1
import { Outlet, Link } from 'react-router';
2
3
const RootLayout = () => {
4
return (
5
<>
6
<nav>
7
<Link to="/">Home</Link>{' '}
8
<Link to="/products">Products</Link>
9
</nav>
10
<Outlet />
11
</>
12
);
13
};
14
export default RootLayout;
설명
1OutletLink를 함께 가져온다
6~8모든 페이지에 공통으로 보이는 네비게이션
10자식 컴포넌트(Home, Products 등)가 이 위치에 렌더링된다

2단계: 라우터에 children으로 연결

src/main.jsx
1
import RootLayout from './pages/RootLayout';
2
3
const router = createBrowserRouter([
4
{
5
path: '/',
6
element: <RootLayout />,
7
children: [
8
{ path: '', element: <Home /> },
9
{ path: 'products', element: <Products /> },
10
],
11
},
12
]);
설명
6RootLayout이 모든 페이지의 틀이 된다
7~10children — 자식 라우트 배열. 부모 레이아웃 안에서 교체된다

이제 Home과 Products 어디서든 네비게이션이 항상 보인다.


3.5. errorElement — 에러 페이지

없는 주소로 접근하거나 오류가 발생했을 때 보여줄 컴포넌트를 errorElement에 등록한다.

1단계: Error 컴포넌트 만들기

src/pages/Error.jsx
1
import { Link } from 'react-router';
2
3
const ErrorPage = () => {
4
return (
5
<div>
6
<h1>404 Error</h1>
7
<p>페이지를 찾을 수 없습니다.</p>
8
<Link to="/">홈으로 돌아가기</Link>
9
</div>
10
);
11
};
12
export default ErrorPage;

2단계: 라우터에 등록

src/main.jsx
1
import ErrorPage from './pages/Error';
2
3
const router = createBrowserRouter([
4
{
5
path: '/',
6
element: <RootLayout />,
7
errorElement: <ErrorPage />,
8
children: [
9
{ path: '', element: <Home /> },
10
{ path: 'products', element: <Products /> },
11
],
12
},
13
]);
설명
7errorElement — 존재하지 않는 경로 접근 시 ErrorPage를 보여준다

주소창에 /없는경로를 입력하면 Error 컴포넌트가 나타난다.


3.6. useNavigate — 코드로 페이지 이동

<Link>는 클릭으로 이동하는 용도고, useNavigate는 버튼 클릭이나 함수 실행 후 코드에서 페이지를 이동할 때 사용한다.

참고

훅(Hook) — 리액트 함수 컴포넌트에서 기능을 사용할 수 있게 해주는 함수다. 이름이 use로 시작한다.

src/pages/Home.jsx
1
import { Link, useNavigate } from 'react-router';
2
3
const Home = () => {
4
const navigate = useNavigate();
5
6
const handleClick = () => {
7
navigate('/products');
8
};
9
10
return (
11
<div>
12
<h1>Home</h1>
13
<button onClick={handleClick}>Products 보러 가기</button>
14
<Link to="/products">링크로 이동</Link>
15
</div>
16
);
17
};
18
export default Home;
설명
1useNavigate를 추가로 가져온다
4useNavigate()가 반환하는 이동 함수를 navigate에 저장한다
6~8버튼 클릭 시 /products로 이동하는 함수
12onClick — 버튼 클릭 시 handleClick을 실행한다

4. 동적 라우팅

4.1. useParams — URL 파라미터 받기

상품 목록에서 상품을 클릭하면 상품마다 다른 상세 페이지로 이동해야 한다. 상품이 100개면 라우트를 100개 만들 수 없으니, :id처럼 변수를 경로에 넣는다.

시나리오:

1
/products → 상품 목록 페이지
2
/products/1 → 1번 상품 상세 페이지
3
/products/2 → 2번 상품 상세 페이지
4
/products/apple → apple 상품 상세 페이지

정보

useParams — use(사용하다) + Params(파라미터, 매개변수). 경로의 동적 값을 읽는 훅이다.

1단계: ProductDetail 컴포넌트 만들기

src/pages/ProductDetail.jsx
1
import { useParams } from 'react-router';
2
3
const ProductDetail = () => {
4
console.log(useParams()); // { id: '1' } 형태로 출력된다
5
6
const { id } = useParams();
7
8
return (
9
<div>
10
<h1>ProductDetail</h1>
11
<p>상품 ID: {id}</p>
12
</div>
13
);
14
};
15
export default ProductDetail;
설명
1useParams를 가져온다
4콘솔에서 파라미터 객체를 확인한다. 예: \{ id: '1' \}
6구조 분해 할당으로 id 값을 꺼낸다
11화면에 URL의 id 값을 출력한다

2단계: 라우터에 동적 경로 등록

:id 뒤의 값은 무엇이든 ProductDetail로 연결된다.

src/main.jsx
1
import ProductDetail from './pages/ProductDetail';
2
3
const router = createBrowserRouter([
4
{
5
path: '/',
6
element: <RootLayout />,
7
errorElement: <ErrorPage />,
8
children: [
9
{ path: '', element: <Home /> },
10
{ path: 'products', element: <Products /> },
11
{ path: 'products/:id', element: <ProductDetail /> },
12
],
13
},
14
]);
설명
11products/:id: 이후의 이름이 변수가 된다. 어떤 값이든 매칭된다

브라우저 주소창에 /products/5 또는 /products/apple을 입력하면 상세 컴포넌트가 렌더링된다.


4.2. 동적 링크 — map으로 링크 자동 생성

상품 데이터 배열을 map으로 반복하면서 각 상품 ID를 Link 경로에 넣는다. 템플릿 리터럴(백틱)로 경로를 동적으로 만든다.

src/pages/Products.jsx
1
import { Link } from 'react-router';
2
3
const PRODUCTS = [
4
{ id: '1', title: '상품1' },
5
{ id: '2', title: '상품2' },
6
{ id: '3', title: '상품3' },
7
];
8
9
const Products = () => {
10
return (
11
<>
12
<h1>Products</h1>
13
<ul>
14
{PRODUCTS.map((item) => (
15
<li key={item.id}>
16
<Link to={`/products/${item.id}`}>{item.title}</Link>
17
</li>
18
))}
19
</ul>
20
</>
21
);
22
};
23
export default Products;
설명
3~7PRODUCTS — 상품 데이터 배열. 실제 서비스에서는 서버에서 받아온다
14~18map() — 배열을 순회하여 각 상품을 <li>로 렌더링한다
16템플릿 리터럴로 /products/1, /products/2 형태의 경로를 동적으로 만든다

4.3. 상대경로 + 뒤로가기

Linkto 속성에서 /로 시작하지 않으면 상대 경로가 된다. ..은 한 단계 위로 이동하는 경로다.

src/pages/ProductDetail.jsx
1
import { useParams, Link } from 'react-router';
2
3
const ProductDetail = () => {
4
const { id } = useParams();
5
6
return (
7
<div>
8
<h1>ProductDetail</h1>
9
<p>상품 ID: {id}</p>
10
<Link to=".." relative="path">← 목록으로 돌아가기</Link>
11
</div>
12
);
13
};
14
export default ProductDetail;
설명
10to=".." — 한 단계 위의 경로로 이동한다
10relative="path" — URL 경로 기준으로 이동한다. 없으면 라우트 계층 기준으로 이동해 홈으로 갈 수 있다

요약

relative="path"를 사용하지 않으면 /products/1에서 ../products가 아닌 /(홈)으로 이동할 수 있다.


4.4. index 라우트 — 기본 페이지 지정

path: '' 대신 index: true를 사용하면 부모 경로(/)에 접속했을 때 보여줄 기본 컴포넌트를 지정할 수 있다.

src/main.jsx
1
const router = createBrowserRouter([
2
{
3
path: '/',
4
element: <RootLayout />,
5
errorElement: <ErrorPage />,
6
children: [
7
{ index: true, element: <Home /> },
8
{ path: 'products', element: <Products /> },
9
{ path: 'products/:id', element: <ProductDetail /> },
10
],
11
},
12
]);
설명
7index: truepath: '' 대신 사용한다. 부모 경로(/) 접근 시 Home을 표시한다

5. useSearchParams — 쿼리스트링 다루기

5.1. useSearchParams란?

URL의 ? 뒤에 붙는 **쿼리스트링(query string)**을 읽고 변경하는 훅이다.

1
/products?category=shoes&sort=price
2
└──────────────────────┘
3
쿼리스트링 부분
용어예시설명
쿼리스트링?category=shoes&sort=priceURL 뒤에 붙는 키=값 쌍
searchParamsURLSearchParams 객체쿼리스트링을 읽는 객체
setSearchParams함수쿼리스트링을 변경하는 함수

5.2. 기본 사용법 — 쿼리스트링 읽기

src/pages/Products.jsx
1
import { useSearchParams } from 'react-router';
2
3
const Products = () => {
4
const [searchParams, setSearchParams] = useSearchParams();
5
6
// /products?category=shoes 일 때
7
const category = searchParams.get('category'); // 'shoes'
8
const sort = searchParams.get('sort'); // null (없으면 null)
9
10
return (
11
<div>
12
<h1>Products</h1>
13
<p>카테고리: {category ?? '전체'}</p>
14
<p>정렬: {sort ?? '기본'}</p>
15
</div>
16
);
17
};
설명
4useSearchParams()는 배열을 반환한다. 첫 번째는 읽기, 두 번째는 변경 함수다
7searchParams.get('키') — 쿼리스트링에서 값을 읽는다. 없으면 null을 반환한다
13?? — null이거나 undefined면 오른쪽 값을 사용한다

5.3. 쿼리스트링 변경하기

setSearchParams에 객체를 전달하면 URL의 쿼리스트링이 바뀐다. 페이지 새로고침 없이 URL만 업데이트된다.

src/pages/Products.jsx
1
import { useSearchParams } from 'react-router';
2
3
const Products = () => {
4
const [searchParams, setSearchParams] = useSearchParams();
5
const category = searchParams.get('category');
6
7
return (
8
<div>
9
<h1>Products</h1>
10
<p>현재 카테고리: {category ?? '전체'}</p>
11
12
<button onClick={() => setSearchParams({ category: 'shoes' })}>
13
신발
14
</button>
15
<button onClick={() => setSearchParams({ category: 'bags' })}>
16
가방
17
</button>
18
<button onClick={() => setSearchParams({})}>
19
전체 보기
20
</button>
21
</div>
22
);
23
};
설명
12버튼 클릭 시 URL이 /products?category=shoes로 바뀐다
18빈 객체를 전달하면 쿼리스트링을 모두 제거한다

5.4. 여러 파라미터 동시 사용

기존 파라미터를 유지하면서 하나만 변경하려면 함수 형태로 전달한다.

src/pages/Products.jsx
1
const Products = () => {
2
const [searchParams, setSearchParams] = useSearchParams();
3
4
const category = searchParams.get('category');
5
const sort = searchParams.get('sort');
6
7
const changeSort = (value) => {
8
setSearchParams((prev) => {
9
prev.set('sort', value);
10
return prev;
11
});
12
};
13
14
return (
15
<div>
16
<h1>Products</h1>
17
<p>카테고리: {category ?? '전체'} / 정렬: {sort ?? '기본'}</p>
18
19
<button onClick={() => setSearchParams({ category: 'shoes', sort: sort || '' })}>
20
신발
21
</button>
22
<button onClick={() => changeSort('price')}>가격순</button>
23
<button onClick={() => changeSort('name')}>이름순</button>
24
</div>
25
);
26
};
설명
8~11prev — 현재 쿼리스트링 객체. set으로 특정 키만 변경하고 반환한다

5.5. 실전 예제 — 검색 + 필터

src/pages/Products.jsx
1
import { useSearchParams, Link } from 'react-router';
2
3
const PRODUCTS = [
4
{ id: '1', title: '운동화', category: 'shoes' },
5
{ id: '2', title: '구두', category: 'shoes' },
6
{ id: '3', title: '백팩', category: 'bags' },
7
{ id: '4', title: '토트백', category: 'bags' },
8
{ id: '5', title: '볼캡', category: 'hats' },
9
];
10
11
const Products = () => {
12
const [searchParams, setSearchParams] = useSearchParams();
13
14
const search = searchParams.get('search') || '';
15
const category = searchParams.get('category') || '';
16
17
const filtered = PRODUCTS.filter((item) => {
18
const matchSearch = item.title.includes(search);
19
const matchCategory = category ? item.category === category : true;
20
return matchSearch && matchCategory;
21
});
22
23
return (
24
<div>
25
<h1>Products</h1>
26
27
<input
28
type="text"
29
placeholder="검색..."
30
value={search}
31
onChange={(e) =>
32
setSearchParams((prev) => {
33
if (e.target.value) prev.set('search', e.target.value);
34
else prev.delete('search');
35
return prev;
36
})
37
}
38
/>
39
40
<div>
41
<button onClick={() => setSearchParams((prev) => { prev.delete('category'); return prev; })}>전체</button>
42
<button onClick={() => setSearchParams((prev) => { prev.set('category', 'shoes'); return prev; })}>신발</button>
43
<button onClick={() => setSearchParams((prev) => { prev.set('category', 'bags'); return prev; })}>가방</button>
44
<button onClick={() => setSearchParams((prev) => { prev.set('category', 'hats'); return prev; })}>모자</button>
45
</div>
46
47
<ul>
48
{filtered.map((item) => (
49
<li key={item.id}>
50
<Link to={`/products/${item.id}`}>{item.title}</Link>
51
</li>
52
))}
53
</ul>
54
{filtered.length === 0 && <p>결과가 없습니다.</p>}
55
</div>
56
);
57
};
58
export default Products;

URL 변화 예시:

1
/products → 전체 상품
2
/products?category=shoes → 신발만 표시
3
/products?search=운동 → "운동" 검색 결과
4
/products?search=운동&category=shoes → 신발 중 "운동" 검색

5.6. useParams vs useSearchParams

구분useParamsuseSearchParams
URL 위치경로 안 (/products/:id)? 뒤 (?key=value)
용도특정 리소스 식별필터, 검색, 정렬
값 변경라우트 이동 필요setSearchParams로 즉시 변경
예시 URL/products/3/products?category=shoes
필수 여부경로에 포함되어야 함선택적 (없어도 됨)

6. 전체 코드

src/main.jsx
1
import React from 'react';
2
import ReactDOM from 'react-dom/client';
3
import { createBrowserRouter, RouterProvider } from 'react-router';
4
import RootLayout from './pages/RootLayout';
5
import Home from './pages/Home';
6
import Products from './pages/Products';
7
import ProductDetail from './pages/ProductDetail';
8
import ErrorPage from './pages/Error';
9
10
const router = createBrowserRouter([
11
{
12
path: '/',
13
element: <RootLayout />,
14
errorElement: <ErrorPage />,
15
children: [
16
{ index: true, element: <Home /> },
17
{ path: 'products', element: <Products /> },
18
{ path: 'products/:id', element: <ProductDetail /> },
19
],
20
},
21
]);
22
23
ReactDOM.createRoot(document.getElementById('root')).render(
24
<RouterProvider router={router} />
25
);
src/pages/RootLayout.jsx
1
import { Outlet, Link } from 'react-router';
2
3
const RootLayout = () => {
4
return (
5
<>
6
<nav>
7
<Link to="/">Home</Link>{' '}
8
<Link to="/products">Products</Link>
9
</nav>
10
<Outlet />
11
</>
12
);
13
};
14
export default RootLayout;
src/pages/Home.jsx
1
import { useNavigate } from 'react-router';
2
3
const Home = () => {
4
const navigate = useNavigate();
5
return (
6
<div>
7
<h1>Home</h1>
8
<button onClick={() => navigate('/products')}>Products 보러 가기</button>
9
</div>
10
);
11
};
12
export default Home;
src/pages/Products.jsx
1
import { Link } from 'react-router';
2
3
const PRODUCTS = [
4
{ id: '1', title: '상품1' },
5
{ id: '2', title: '상품2' },
6
{ id: '3', title: '상품3' },
7
];
8
9
const Products = () => {
10
return (
11
<>
12
<h1>Products</h1>
13
<ul>
14
{PRODUCTS.map((item) => (
15
<li key={item.id}>
16
<Link to={`/products/${item.id}`}>{item.title}</Link>
17
</li>
18
))}
19
</ul>
20
</>
21
);
22
};
23
export default Products;
src/pages/ProductDetail.jsx
1
import { useParams, Link } from 'react-router';
2
3
const ProductDetail = () => {
4
const { id } = useParams();
5
return (
6
<div>
7
<h1>ProductDetail</h1>
8
<p>상품 ID: {id}</p>
9
<Link to=".." relative="path">← 목록으로 돌아가기</Link>
10
</div>
11
);
12
};
13
export default ProductDetail;
src/pages/Error.jsx
1
import { Link } from 'react-router';
2
3
const ErrorPage = () => {
4
return (
5
<div>
6
<h1>404 Error</h1>
7
<p>페이지를 찾을 수 없습니다.</p>
8
<Link to="/">홈으로 돌아가기</Link>
9
</div>
10
);
11
};
12
export default ErrorPage;

페이지 흐름:

1
/ (Home)
2
└─ 버튼 클릭 → /products (상품 목록)
3
├─ 상품1 클릭 → /products/1 (상세)
4
├─ 상품2 클릭 → /products/2 (상세)
5
└─ 상품3 클릭 → /products/3 (상세)
6
└─ 뒤로가기 → /products (목록)