라우팅이란 사용자가 특정 URL을 방문했을 때 그 URL에 맞는 페이지나 컴포넌트를 사용자에게 보여주는 기능을 의미한다.
React는 기본적으로 싱글 페이지 어플리케이션(SPA)이기 때문에 서버는 하나의 html파일만을 갖고있다.
따라서 리액트에서 라우팅은 URL이 변경될때마다 새로운 html파일을 로딩하지 않고 해당 URL에 맞는 컴포넌트를 사용자에게 보여준다.
이러한 라우팅은 react-router-dom이라는 라이브러리를 통해 쉽게 구현할 수 있다.
react-router-dom을 통해 라우트를 정의하는 방법은 다음과 같다.
const router = createBrowserRouter([
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> }
...
...
]);
createBrowserRouter의 인자로 path와 element등을 가지는 객체를 배열로 넣어준 뒤 해당 router를 사용하면 된다.
path에 해당하는 URL을 방문시 해당 element의 컴포넌트를 렌더링해준다.
이렇게 만든 router를 App컴포넌트에서 RouterProvider를 사용하여 연결해준다.
...
...
function App() {
return <RouterProvider router={router}></RouterProvider>;
}
만약 특정 경로로 시작하는 모든 경로에 같은 컴포넌트를 보여주고 싶다면 어떻게 해야할까?
예를들어 모든 페이지에 네비게이션 바를 상단에 추가하는것처럼 말이다.
네비게이션 바를 컴포넌트로 만들고, 모든 페이지에 해당 컴포넌트를 사용할수도 있지만 페이지가 많아지면 똑같은 추가를 계속 반복해야 한다.
따라서 react-router-dom은 중첩 라우팅이라는 기능을 제공한다.
중첩 라우팅은 특정 라우트를 감싸는 컴포넌트를 추가하고, 해당 라우트의 하위 라우트는 모두 감싸진 레이아웃 컴포넌트 내부의 특정 위치에 로딩되는 방식이다.
이러한 중첩 라우팅을 구현하기 위해서는 createBrowserRouter를 통해 라우팅 객체를 생성할 때 children속성을 통해 구현할 수 있다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> }
]
}
]);
위의 코드를 자세히 보면, 기본 경로인 "/"는 element로 <RootLayout />이라는 컴포넌트를 갖는다.
또한 해당 경로에는 children을 통해 새로운 경로 객체 배열을 정의해준다.
이제 <RootLayout />컴포넌트를 살펴보면 다음과 같다.
import MainNavigation from "../components/MainNavigation";
import { Outlet } from "react-router-dom";
const RootLayout = () => {
return (
<>
<MainNavigation />
<main>
<Outlet />
</main>
</>
);
};
export default RootLayout;
공통적으로 사용될 컴포넌트인 <MainNavigation />컴포넌트를 갖고있고, 그 아래에 main으로 감싸진 <Outlet />컴포넌트가 존재한다.
<Outlet />컴포넌트는 react-router-dom이 제공하는 특수한 컴포넌트로, children을 통해 넘어오는 경로들의 element가 렌더링 될 위치를 지정한다.
만약 사용자가 /products페이지를 방문하면 해당 페이지의 컴포넌트 구조는 다음과 같다.
<>
<MainNavigation />
<main>
<ProductsPage />
</main>
</>
사용자가 존재하지 않는 URL을 통해 접속을 시도할 경우 오류 페이지를 렌더링 해야하는데, 일단 react-router-dom은 기본 오류 페이지를 제공하며, 커스텀된 페이지를 적용하는 것도 가능하다.
특정 경로로 시작하는 페이지의 경로를 잘못 입력했을 때 오류페이지를 렌더링하는 방법은 다음과 같이 errorElement속성을 이용하는 방법이 있다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> }
]
}
]);
errorElement에 지정된 컴포넌트는 사용자가 해당 경로로 시작하는 URL중 존재하지 않는 경로로 이동시에 자동으로 렌더링된다.
우리가 웹 페이지를 사용하다보면 링크가 동적으로 생성되는 경우가 존재한다.
예를들어 블로그에 글을 작성하게 될 경우, 해당 글로 들어올 수 있는 새로운 URL이 생성된다.
이러한 동적으로 생성되는 페이지를 라우팅하는 기능 역시 react-router-dom에서 제공해준다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "/", element: <HomePage /> },
{ path: "/products", element: <ProductsPage /> },
{ path: "/products/:productId", element: <ProductDetailPage /> }
]
}
]);
children속성의 3번째 경로를 보면 path: "/products/:productId"과 같이 입력된 것을 확인할 수 있다.
맨 뒷쪽에 :으로 시작하는 부분을 경로 세그먼트라 부르는데, 해당 부분을 통해 동적 라우팅이 가능해진다.
ProductDetailPage는 제품의 상세정보를 보여주는 페이지로, 제품을 추가할때마다 새로운 페이지가 추가되어야 한다.
해당 컴포넌트에서는 경로 세그먼트를 이용하여 데이터를 받아오고, 해당 데이터를 렌더링 해주는 역할을 수행한다.
import { Link, useParams } from "react-router-dom";
import { ParamsType } from "../types/types";
const ProductDetailPage = () => {
const params = useParams<ParamsType>();
return (
<>
<h1>Product Details!</h1>
<p>{params.productId}</p>
</>
);
};
export default ProductDetailPage;
세그먼트에 넣어준 부분은 react-router-dom이 제공하는 useParams라는 hook을 통해 객체 형태로 받을 수 있으며, 해당 params 객체에서 추출하여 사용할 수 있다.
현재 지금까지 작성한 모든 경로는 "/"로 시작하는 절대경로로 작성되어 있다.
/로 시작하는 경로는 도메인 이름 뒤에서부터 현재 페이지까지 모든 경로를 적어주어야 한다.
하지만 상대 경로는 해당 페이지의 부모 URL을 기준으로 동작한다.
const router = createBrowserRouter([
{
path: "/root",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "", element: <HomePage /> },
{ path: "products", element: <ProductsPage /> },
{ path: "products/:productId", element: <ProductDetailPage /> }
]
}
]);
만약 위와같이 작성하게되면 products의 경로는 /root/products가 된다.
웹에서 일반적으로 다른 페이지로 이동할 때 사용되는 태그는 a태그가 존재한다.
하지만 해당 태그를 사용하여 이동을 하면 서버에 새로운 요청을 보내게 되고, 리액트 애플리케이션을 새로 로딩하는 작업을 하게된다.
따라서 이러한 요청을 방지하기 위해서는 react-router-dom에서 제공하는 특수한 컴포넌트를 사용해야 한다.
react-router-dom을 이용하여 라우팅을 할 때 앵커 태그 대신 사용하는 컴포넌트로, href속성 대신 to속성을 사용하여 경로를 설정한다.
해당 컴포넌트를 사용하면 새로운 HTTP요청을 전송하지 않아 페이지가 새로고침되지 않고 페이지만 업데이트하기 때문에 사용시에 끊김이 없는 SPA의 이점을 사용가능한다.
<Link to={"/products"}>the list of products</Link>
NavLink컴포넌트는 기본적으로 Link와 같은 기능을 수행하지만, className과 같은 프로퍼티를 통해 스타일을 지정할 때 해당 프로퍼티는 함수를 받게된다.
해당 함수는 isActive, isPending, isTransitioning을 포함하는 객체를 인자로 받고, 여기서는 isActive를 통해 스타일을 업데이트 할 수 있다.
<NavLink
to={"/"}
className={({ isActive }) => (isActive ? classes.active : undefined)}
end
>
위의 코드와 같이 사용하면, 현재 경로가 활성화 되었을 경우 스타일에 css의 active스타일을 추가할 수 있게된다.
맨 마지막에 인자로 end를 받는 것을 볼 수 있는데, 해당하는 프로퍼티를 사용하지 않으면 해당 링크가 포함된 모든 링크가 활성화상태가 되게 된다.
예를들어 /products로 이동하여도 해당 경로는 /를 포함하고 있기 때문에 두 경로 모두 active상태가 되어버린다.
이러한 것을 방지하기 위해 end프로퍼티를 true로 지정할 경우 해당하는 경로로 끝나는 경우에만 active상태를 반환한다.
relative프로퍼티를 사용하여 뒤로가기 구현
Link나 NavLink를 통해 경로를 지정할때는 상대경로, 절대경로 모두 사용할 수 있으며, '..'을 통해서 한단계 뒤로가는 경로도 설정할 수 있다.
이 뒤로가기 기능은 기본적으로 현재 경로의 부모 경로로 이동하게 된다.
const router = createBrowserRouter([
{
path: "/root",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ path: "", element: <HomePage /> },
{ path: "products", element: <ProductsPage /> },
{ path: "products/:productId", element: <ProductDetailPage /> }
]
}
]);
위와 같이 경로가 정의된 상황에서 <ProductDetailPage />에 해당하는 /products/p1에서 Link 또는 NavLink의 '..'경로를 통해 뒤로 이동하면 /products페이지로 이동하는 것이 아닌 해당 경로의 부모 경로인 /로 이동하게 된다.
이러한 동작을 바로 이전 경로인 /products로 이동하고 싶을 경우에는 relative프로퍼티를 path로 지정해주면 된다.
Link 혹은 NavLink를 통해 클릭하여 페이지를 이동하는 것 말고도 useNavigate를 사용하여 프로그래밍적으로 페이지를 이동하는 방법도 존재한다.
const navigate = useNavigate();
const navigateHandler = () => {
navigate("/products");
};
위와 같이 react-router-dom이 제공하는 useNavigate hook을 통해 navigateHandler함수가 실행되는 시점에서 강제로 라우팅이 가능하다.