티스토리 뷰

기록

React Router의 layout 적용기

als982001 2025. 6. 28. 18:22

1. 개요

 노마드코더의 Maker 마스터클래스 강의 중, 특정 product의 overview와 review를 보여주는 페이지를 만드는 영상을 본 후, layout이라는 꽤나 괜찮은 기능이 있어서 기록하려 한다.

 

// app/routes.ts
...prefix("/:productId", [
      index("features/products/pages/product-redirect-page.tsx"),
      route("/overview", "features/products/pages/product-overview-page.tsx"),
      ...prefix("/reviews", [
        index("features/products/pages/product-reviews-page.tsx"),
        route("/new", "features/products/pages/new-product-review-page.tsx"),
      ]),
    ]),

 

routes.ts를 보면 알 수 있듯, /:productId/overview에서는 상품의 개요를, /:product/reviews에서는 상품의 리뷰를 보여준다.

 

overview

 

reviews

 

그리고 위 이미지는 상품의 overview 페이지와 reviews 페이지의 뷰인데, 노란색으로 감싼 부분을 reviews에서도 동일하게 보여주고 싶다. 이를 위해 그냥 중복된 코드를 작성하거나, 한 페이지에서 overview를 보여줄지, reviews를 보여줄지를 state 등을 이용해서 구현하는 등의 방법도 있지만, React Router의 layout을 통해 이를 간단히 구현할 수 있었다.

 

2. React Router의 layout이란

 React Rotuer의 layout은 여러 페이지(혹은 경로)가 공통으로 공유하는 UI 구조(레이아웃)을 한 곳에서 관리할 수 있게 도와주는 기능이다. 이를 통해 특정 경로 아래의 모든 하위 페이지에 자동으로 레이아웃이 적용되게 할 수 있다.

 

3. layout 적용

3-1. 레이아웃 컴포넌트 작성

 우선, 레이아웃 컴포넌트를 만들어야 한다. 이 상황에서 공유하고 싶은 레이아웃은 위 이미지의 헤더 부분이다. 그래서 저 부분의 코드를 overview 컴포넌트에서 복사한 후, 아래처럼 붙여넣기했다.

 

// app/features/products/layouts/product-overview-layout.tsx
import { NavLink, Outlet } from "react-router";
import { ChevronUpIcon, StarIcon } from "lucide-react";

import { Button, buttonVariants } from "~/common/components/ui/button";
import { cn } from "~/lib/utils";

export default function ProductOverviewLayout() {
  return (
    <div className="space-y-10">
      <div className="flex justify-between">
        <div className="flex gap-10">
          <div className="size-40 rounded-xl shadow-xl bg-primary/50"></div>
          <div>
            <h1 className="text-5xl font-bold">Product Name</h1>
            <p className="text-2xl font-light">Product Description</p>
            <div className="mt-5 flex item-center gap-2">
              <div className="flex text-yellow-400">
                {Array.from({ length: 5 }).map((_, i) => (
                  <StarIcon className="size-4" fill="currentColor" key={i} />
                ))}
              </div>
              <span className="text-muted-foreground">100 reviews</span>
            </div>
          </div>
        </div>
        <div className="flex gap-5">
          <Button
            variant={"secondary"}
            size="lg"
            className="text-lg h-14 px-10"
          >
            Visit Website
          </Button>
          <Button size="lg" className="text-lg h-14 px-10">
            <ChevronUpIcon className="size-4" />
            Upvote (100)
          </Button>
        </div>
      </div>
      <div className="flex gap-2.5">
        <NavLink
          className={({ isActive }) =>
            cn(
              buttonVariants({ variant: "outline" }),
              isActive && "bg-accent text-foreground "
            )
          }
          to={`/products/1/overview`}
        >
          Overview
        </NavLink>
        <NavLink
          className={({ isActive }) =>
            cn(
              buttonVariants({ variant: "outline" }),
              isActive && "bg-accent text-foreground "
            )
          }
          to={`/products/1/reviews`}
        >
          Reviews
        </NavLink>
      </div>
      <div>
        <Outlet /> {/* Outlet을 통해 하위 라우트가 렌더링된다 */}
      </div>
    </div>
  );
}

 

코드의 끝부분을 보면 Outlet이라는 것이 있는데, 이 Outlet에 통해 하위 라우트의 컴포넌트들이 렌더링되게 된다. 추가로, 기존의 overview 컴포넌트에서 위 헤더 부분을 제거해야 헤더가 중복되지 않게 보인다.

 

3-2. routes.ts에서 layout 사용

// app/routes.ts

route("/promote", "features/products/pages/promote-page.tsx"), // <- 여기까지는 동일
...prefix("/:productId", [
      index("features/products/pages/product-redirect-page.tsx"),
      layout("features/products/layouts/product-overview-layout.tsx", [
        route("/overview", "features/products/pages/product-overview-page.tsx"),
        ...prefix("/reviews", [
          index("features/products/pages/product-reviews-page.tsx"),
          route("/new", "features/products/pages/new-product-review-page.tsx"),
        ]),
      ]),
    ]),
  ]),
] satisfies RouteConfig;

 

 기존의 route("/overview", ...) + ...prefix("/reviews", ...) 부분에 layout을 적용한다. layout의 각 파라미터는 다음과 같다.

  • 첫 번째 파라미터: 레이아웃 컴포넌트의 파일 경로
  • 두 번째 파라미터: 해당 레이아웃의 Outlet에 렌더링할 자식 routes 배열

 

3-3. 렌더링 결과

Overview

 

/products/1/overview에 진입하면 상단의 헤더 UI(레이아웃) + overview 컴포넌트가 동시에 보인다.

 

Reviews

 

/products/1/reviews에 진입해도 상단의 헤더 UI(레이아웃) + overview 컴포넌트가 동시에 보인다.

 

4. 결론

 React Router의 layout은 여러 라우트에서 공통으로 사용하는 UI(탭, 메뉴, 사이드바 등)를 한 곳에서 관리할 수 있게 해주는 강력한 도구로, 중복 UI를 제거할 수 있고, 유지보수가 보다 용이하며, 중첩 라우트와 잘 어울린다는 점이 정말 좋다고 할 수 있다.

 

 


참고 문서

- React Router의 Routing

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함