앱이 복잡해질 수록 웹사이트/웹 앱의 특정 부분에 바로 링크되면 좋다.
웹사이트 특정 부분 방문 시 그 부분을 로딩하는 URL을 제공하는 것이다.
SPA 라우팅이 바로 그 부분에 사용된다.
리액트 라우터를 사용하면 매번 백엔드에서 새로운 페이지를 가져오지 않고 다양한 URL로 원하는 페이지를 방문할 수 있다.
라우팅을 이해하려면 웹이 전반적으로 어떻게 작동하는지 이해해야 한다.
웹사이트를 방문하면 일반적으로 도메인 이름 뒤에 경로를 첨부할 수 있다.
https://blog.naver.com/userId
처럼 말이다. 이 url은 해당 유저의 네이버 블로그 페이지를 로딩한다.
다른 유저의 아이디를 입력하면 그 유저의 블로그 페이지가 로딩된다.
이렇게 웹사이트에 표시되는 콘텐츠가 변경된다.
이것이 라우팅이 하는 일이다.
url 경로가 다르면 다른 콘텐츠가 화면에 로딩된다.
현재까지는 다른 콘텐츠에 대해 다른 HTML을 로딩하는 방식으로 구현했다.
이 방법은 단점이 몇 가지 있다.
그래서 더 복잡한 사용자 인터페이스를 구축할 때 SPA(싱글 페이지 어플리케이션)를 사용한다.
그러면 최초 HTML 요청을 하나만 전송하는데, html 파일과 추가로 많은 JavaScript가 다운로드되고, 클라이언트에서 실행되는 추가 JavaScript 코드는 사용자가 화면에서 보는 것들을 실제로 조절하게 된다.
npm i react-router-dom
지원하려는 라우트 정의
지원하려는 URL과 경로, 다양한 경로에 대해 어떤 컴포넌트가 로딩되어야 하는지 정의
라우터 활성화하여 1에서 정의한 라우트를 로딩
로딩하려는 모든 컴포넌트들이 있는지 확인하고 페이지들 간에 이동할 수단 제공했는지 확인
위 세 단계가 잘 되었다면 사용자들은 다양한 페이지들 사이를 매끄럽게 이동할 수 있다.
이제 단계별로 살펴보자.
루트 컴포넌트인 App.js에서 react-router-dom 패키지로부터 createBrowserRouter 가져오기
import { createBrowserRouter } from "react-router-dom";
라우터 정의
createBrowserRouter([{}, {}, {}, ...]);
createBrowserRouter로 객체로 된 어레이를 보내는데, 각각의 객체는 각각 하나의 라우트를 나타낸다.
라우트를 구성하는 몇 가지 프로퍼티가 있다.
createBrowserRouter([{ path: "/", element: <Home/> }]);
"/"
, "/userId"
)/src/pages
pages 폴더를 프로젝트에 추가하여 라우터에 의해 페이지로서 로딩될 컴포넌트를 담자.
폴더이름은 /pages
, /components
, /routes
뭘로 하든 상관 없지만 직관적이게 /pages
를 많이 쓰나보다.
라우터를 사용하기 위해서는 createBrowserRouter함수의 리턴 값을 변수/상수에 저장해야 한다.
const router = createBrowserRouter([{ path: "/", element: <Home /> }]);
라우터 활성화 하기
import { RouterProvider, createBrowserRouter } from "react-router-dom";
function App() {
return <RouterProvider router={router} />;
//이렇게 router를 활성화 한다. 이제 router는 우리 url을 관찰하녀 현재 활성인 경로가 무엇인지 확인하여 element를 로딩한다.
}
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Home from "./pages/Home";
const router = createBrowserRouter([{ path: "/", element: <Home /> }]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
(참고) React Router 6.4 버전 이하에서는 아래 처럼 사용하기도 했다.
나도 작년엔 이거 사용했었음..
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements, } from "react-router-dom"; import Home from "./pages/Home"; import Products from "./pages/Products"; // const routeDefinitions = createRoutesFromElements( <Route> <Route path="/" element={<Home />} /> <Route path="/products" element={<Products />} /> </Route> ); // const router = createBrowserRouter(routeDefinitions); // function App() { return <RouterProvider router={router} />; } // export default App;
<a href="/products">go to list of products.</a>
로 링크를 걸어 페이지를 이동할 수도 있다.
<Link to="/products">링크: 프로덕트로 이동!</Link>
import { Link } from "react-router-dom";
const Home = () => {
return (
<>
<h1>My Home Page</h1>
<Link to="/products">링크: 프로덕트로 이동!</Link>
</>
);
};
export default Home;
상단에 네비게이션 바를 추가하자. 대부분의 웹사이트는 이동이 쉽도록 네비게이션 바가 있다. ㅇㅇ
/components
폴더를 만들어 네비게이션 컴포넌트를 만들자.
네비게이션을 어디에서 렌더링해야 할까가 문제이다.
RouterProvider 밖에서 네비게이션을 렌더링한다면 Link를 사용할 수가 없다.
그렇기 때문에 가장 상단에 네비게이션을 렌더링하는 랩퍼 컴포넌트를 두고 그 자식으로 홈, 프로덕트 라우트를 중첩해서 사용해 보자.
/pages/Root.js
를 만들고 네비게이션을 가져온다.
import MainNavigation from "../components/MainNavigation";
const RootLayout = () => {
return <MainNavigation />;
};
export default RootLayout;
"/"를 패스로 갖고 RootLayout을 요소로 갖는 새 라우트 객체를 만들고, children 프로퍼티에 홈, 프로덕트 라우트를 중첩하여 배열로 넣는다.
이렇게 홈, 프로덕트 라우트를 루트레이아웃 라우트의 자녀 라우트로 넣어주면, 부모 라우트인 루트레이아웃은 wrapper 역할을 하게된다.
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import Home from "./pages/Home";
import Products from "./pages/Products";
import RootLayout from "./pages/Root";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{ path: "/", element: <Home /> },
{ path: "/products", element: <Products /> },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
Outlet 컴포넌트로 자녀 라우트를 렌더링할 장소를 표시할 수 있다.
import { Outlet } from "react-router-dom";
import MainNavigation from "../components/MainNavigation";
const RootLayout = () => {
return (
<>
<MainNavigation />
<Outlet />
</>
);
};
export default RootLayout;
<Home/>
, <Products/>
가 렌더링 된다.이렇게 레이아웃 역할을 하는 루트 라우트를 만드는 것은 리액트 라우터를 사용할 때 아주 표준적이고 정상적인 방법이다.
더 복잡한 페이지의 경우, 다수의 루트 라우트를 만들고 각 루트 라우트 별로 children을 추가하여 자식들을 감싸는 다른 레이아웃을 가질 수 있다. 이것이 장점 ㅇㅇ!
설정하지 않은 페이지 방문 시 이렇게 오류 페이지가 뜨는데, 이 기본 오류 메시지는 react-router-dom 패키지가 생성한 메시지이다.
방문자들이 방문하지 말아야할 페이지나 존재하지 않는 페이지로 접근하는 것을 방지하기 위해, 기본 오류 페이지를 준비해 보자.
먼저 /pages/Error.js
에러페이지를 만든다.
import MainNavigation from "../components/MainNavigation";
function ErrorPage() {
return (
<>
<MainNavigation />
<main>
<h1>에러 발생!</h1>
<p>페이지를 찾을 수 없습니다.</p>
</main>
</>
);
}
export default ErrorPage;
errorElement 프로퍼티 추가하여 에러 발생시 뱉을 에러 페이지 추가할 수 있다.
//...
import ErrorPage from "./pages/Error";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
//errorElement 프로퍼티 추가하여 에러 발생시 뱉을 에러 페이지 추가할 수 있다.
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
],
},
]);
//...
Link 대신 NavLink를 사용하면 네비게이션에서 현재 있는 url, 즉 활성 상태인 링크를 하이라이팅 할수 있는 active 클래스를 사용할 수 있다.
Link 컴포넌트는 일반적인 앵커 요소를 렌더링하기 때문에 a앵커 태그에 스타일을 추가하면 된다.
.list a {
text-decoration: none;
color: var(--color-primary-400);
}
.list a:hover,
.list a.active {
color: var(--color-primary-800);
text-decoration: underline;
}
NavLink에서는 className 프로퍼티가 문자열을 받는 일반적인 className 프로퍼티가 아닌, 함수를 받는 className 프로퍼티이다.
객체{}
를 받는데, 거기에 isActive 프로퍼티
를 할당하여 isActive인지에 따라 앵커 태그에 추가되어야 하는 클래스 이름을 리턴하여 하이라이팅을 줄 수 있다.<NavLink
className={({ isActive }) => (isActive ? classes.active : undefined )}
to="/"> Home
</NavLink>
import { NavLink } from "react-router-dom";
import classes from "./MainNavigation.module.css";
const MainNavigation = () => {
return (
<header className={classes.header}>
<nav>
<ul className={classes.list}>
<li>
<NavLink
className={({ isActive }) => (isActive ? classes.active : undefined)}
to="/"
>
Home
</NavLink>
</li>
<li>
<NavLink
className={({ isActive }) => (isActive ? classes.active : undefined)}
to="/products"
>
Products
</NavLink>
</li>
</ul>
</nav>
</header>
);
};
export default MainNavigation;
(참고) 인라인 스타일로 isActive 인지 확인하여 클래스 추가할 수도 있다.
하지만 굳이..! 😇
"/" 로 시작하는 모든 라우트에 대해 활성상태가 되어버리는 것을 방지하기 위해서 end
프로퍼티를 사용하자.
<NavLink
className={({ isActive }) =>isActive ? classes.active : undefined}
to="/"
end
>
import { useNavigate } from "react-router-dom";
const HomePage = () => {
const navigate = useNavigate();
//네비게이션 동작 트리거 할 수 있고, 프로그램적으로 코드 안에서 다른 라우트로 전환할 수 있음
const navigateHandler = () => {
//navigate 함수 호출하여 이동할 라우트 작성
navigate("/products");
};
return (
<>
<h1>My Home Page</h1>
<button onClick={navigateHandler}>Go To Products</button>
</>
);
};
export default HomePage;
보통 useNavigate 훅을 사용하여 버튼 클릭으로는 페이지를 이동하지는 않지만, 사용법을 간단히 익히기 위해 버튼을 클릭 시 함수를 트리거하여 useNavigate로 "/products" 페이지로 넘어가게 했다.
제품 페이지로 가면 보통 많은 제품 리스트(ul)가 있다.
const ProductsPage = () => {
return (
<>
<h1>Products</h1>;
<ul>
<li>P1</li>
<li>P2</li>
<li>P3</li>
</ul>
</>
);
};
export default ProductsPage;
그리고 다양한 제품에 대한 별도의 세부 정보 페이지 로딩할 수 있게 링크를 걸어둔다.(li)
보통 제품 상세페이지에 대한 컴포넌트에 프로덕트 정보를 매핑하여 사용하게 되는데 이럴 때 경로(path)는 어떻게 해야 할까?
const ProductDetail = () => {
return (
<>
<h1>Product Detail</h1>
</>
);
};
export default ProductDetail;
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import HomePage from "./pages/Home";
import ProductsPage from "./pages/Products";
import ProductDetail from "./pages/ProductDetail";
import RootLayout from "./pages/Root";
import ErrorPage from "./pages/Error";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
// 콜론은 경로의 이 부분이 dynamic 하다는 사실을 리액트 라우터 돔에게 알려준다.
{ path: "/products/:id", element: <ProductDetail /> },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
리액트 라우터 돔은 역동적 path 세그먼트, 경로 파라미터를 지원한다.
콜론(:)
은 경로의 이 부분이 dynamic 하다는 사실을 리액트 라우터 돔에게 알려준다.이제 어떤 제품에 대한 ProductDetail 페이지가 로딩되었는지 알기 위해 useParams()을 사용하여 플레이스 홀더, 즉 :id
대신 사용된 실제값을 가져 올 수 있다.
import { useParams } from "react-router-dom";
const ProductDetail = () => {
const params = useParams();
const id = params.id;
return (
<>
<h1>Product Detail</h1>
<p>{id}</p>
</>
);
};
export default ProductDetail;
const params = useParams();
이렇게 url에 인코딩된 데이터를 잡을 수 있다.
일반적으로 품목이나 제품의 id같은 것을 url에 인코딩하여 사용한다.
import { Link } from "react-router-dom";
//보통 백엔드에서 데이터를 받아오겠지만 여기선 일단 더미데이터 생성
const PRODUCTS_DUMMY = [
{ id: "p1", title: "Product 1" },
{ id: "p2", title: "Product 2" },
{ id: "p3", title: "Product 3" },
];
const ProductsPage = () => {
return (
<>
<h1>Products</h1>
<ul>
{PRODUCTS_DUMMY.map((prod) => (
<li key={prod.id}>
<Link to={`/products/${prod.id}`}>{prod.title}</Link>
</li>
))}
</ul>
</>
);
};
export default ProductsPage;
이렇게 역동적 경로 파라미터가 있는 라우트에 대해 링크를 생성하고 구축할 수 있다.
라우트는 작동해야 하는 경로이다.
현재 아래처럼 라우트를 설정하고 있다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
{ path: "/products/:id", element: <ProductDetail /> },
],
},
]);
여기서 정의하는 모든 경로는 /
로 시작하는 절대 경로(absolute paths)이다.
이 의미는 항상 도메인 이름 뒤에서 부터 나타난다는 의미로, 중요한 세부 정보이다.
예를 들어 래퍼 경로를 "/root"
로 변경하여 재로딩 하면 아무 페이지도 안뜬다.
const router = createBrowserRouter([
{
path: "/root",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
{ path: "/products/:id", element: <ProductDetail /> },
],
},
]);
url에 "/root"
, "/root/products"
, "/products"
를 모두 입력해 봐도 모두 빈 화면만 나오는데 그 이유는 절대경로인 "/"
가 "/root"
에 중첩되었기 때문이다.
이때 자녀 라우트 앞에 있는 /를 제거하면 라우트 정의 경로가 상대 경로로 변한다.
그러면 자동으로 부모 라우트인 래퍼 라우트의 "/root" 경로 뒤에 첨부된다.
즉, 현재 활성 경로 뒤에 첨부된다.
const router = createBrowserRouter([
{
path: "/root",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "", element: <HomePage /> },
{ path: "products", element: <ProductsPage /> },
{ path: "products/:id", element: <ProductDetail /> },
],
},
]);
이렇게 상대 경로로 된 자녀 라우트가 있다면 리액트 라우터는 부모 라우트의 경로를 살펴보고 자녀 라우트를 부모 라우트 경로의 뒤에 첨부하므로 에러가 발생하지 않는다.
따라서 나머지 페이지와 네비게이션에서도 /
를 삭제하여 모두 절대경로가 되게 하면 잘 작동한다.
/products/
는 빼고 제품의 아이디만 적어주면 된다. 왜냐하면 상대경로를 상요하면 현재 활성 경로 뒤에 첨부되기 때문이다.<ul>
{PRODUCTS_DUMMY.map((prod) => (
<li key={prod.id}>
<Link to={prod.id}>{prod.title}</Link>
</li>
))}
</ul>
<Link to={
product/${prod.id}}>
라고 적을 경우 url에 /product/product/id
이렇게 프로덕트가 두 번 표시되어 버리니 주의!제품 상세 페이지에서 뒤로가기 링크를 추가하여 뒤로가기를 클릭했을 때 상품 리스트 페이지로 돌아가도록 해보자.
import { Link, useParams } from "react-router-dom";
const ProductDetail = () => {
const params = useParams();
const { id } = params;
return (
<>
<h1>Product Detail</h1>
<p>{id}</p>
<p>
<Link to="..">
뒤로가기
</Link>
</p>
</>
);
};
export default ProductDetail;
<Link to="..">
..
은 활성이었던 경로나 라우트로 돌아간다는 뜻이다.그런데 뒤로가기를 클릭하니 홈으로 가버린다.
/products/p1, 두개의 세그먼트가 모두 url 경로에서 제거되어 버린다.
왜냐하면 상대 경로인 .. 가 라우트 정의에 대해 상대적으로 리졸빙되었기 때문이다.
ProductDetailPage 라우트의 정의는 / 라우트의 자녀이고, products의 형제이다.
그래서 한 수준 위로 올라가면 / 홈인것이다.
이전 라우트 경로로 돌아간다면 형제가 아닌 부모의 라우트 경로로 돌아간다.
따라서 부모 경로가 아닌 한 세그먼트만 제거하기 위해서 Link의 특수 프로퍼티인 relative 프로퍼티를 추가하여 사용하면 된다.
relative=route
relative 프로퍼티의 기본값은 route로 라우트 정의에 대해 상대적이다.
이 세그먼트를 현재 활성인 라우트 경로에 대해 상대적으로 추가/제거한다.
relative=path
path로 설정하면 리액트 라우터는 현재 활성인 경로를 살펴보고, 그 경로에서 한 세그먼트만 제거한다.
따라서 URL에서 현재 활성인 경로에 대해 한 세그먼트를 추가/제거 제어할 수 있다.
(참고) 절대 경로가 있으면 항상 그 절대 경로가 도메인 뒤에 추가되기 때문에 relative 프로퍼티는 작동하지 않는다.
import { Link, useParams } from "react-router-dom";
const ProductDetail = () => {
const params = useParams();
const { id } = params;
return (
<>
<h1>Product Detail</h1>
<p>{id}</p>
<p>
<Link to=".." relative="path">
뒤로가기
</Link>
</p>
</>
);
};
export default ProductDetail;
이렇게 하면 뒤로가기 클릭 시 현재 url에서 활성인 경로에 대해 한 세그먼트만 제거되므로 홈이 아닌 상품 리스트 페이지로 돌아갈 수 있다.
상대/절대 경로와 관련된, 라우트 정의에 추가할 수 있는 특수 프로퍼티가 있다.
이 프로퍼티는 일부 라우트 정의에 추가할 수 있는데, 예를 들면 HomePage에 적용할 수 있다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "", element: <HomePage /> },
{ path: "products", element: <ProductsPage /> },
{ path: "products/:id", element: <ProductDetail /> },
],
},
]);
HomePage 라우트 정의에는 보다시피 경로가 없다. 대신 부모 라우트인 RootLayout에 있는 경로와 동일한 경로로 로딩된다.
현재 래핑라우트가 필요하기 때문에 같은 경로를 가지는 두개의 라우트가 있는 것이다.
이런 경우, 즉 부모와 같은 경로를 가지는 자녀 라우트에 특수 프로퍼티인 index 프로퍼티를 추가하여 설정할 수 있다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <HomePage /> },
{ path: "products", element: <ProductsPage /> },
{ path: "products/:id", element: <ProductDetail /> },
],
},
]);
그러면 HomePage 라우트는 소위 인덱스 라우트로 변한다.
부모 라우트가 활성일 경우 표시되어야 하는 기본 라우트가 이 라우트라는 의미이다.
따라서 /
에 있으면 인덱스 라우트인 HomePage가 활성화 된다.
인덱스 라우트를 꼭 써야 하는건 아니지만 가끔 이런 기본 라우트가 필요하다.
부모와 자녀 라우트가 경로가 같다면, 빈 경로를 추가하는 대신 인덱스 프로퍼티를 사용하여 같은 기능을 구현할 수도 있다는 것을 알아두자.