Next.js Parallel Routes와 query string으로 master/detail 형태의 페이지 구현하기

2025년 12월 8일 월요일
1456글자
8분

0️⃣ Master/Detail UI

이번에 맡은 팀 프로젝트에서 할 일 목록을 표출하는 페이지와 그 할 일 목록의 상세 내용을 표출하는 페이지를 구현을 맡게 되었습니다.

해당 페이지는 Master/Detail 패턴으로 구현해야 했습니다.
여기서 Master/Detail 패턴은 왼쪽에는 목록을, 오른쪽에는 상세 내용을 표출하는 UI를 의미합니다.

master detail pattern - windows blog

해당 UI를 구현하기 위해 Next.js의 Parallel Routes를 이용해보겠습니다.
지난번에도 Parallel Routes를 사용하여 페이지를 제작했었는데 이 때는 모달의 형태로 표출된 페이지를 새로고침을 하게 되면 모달이 전체 페이지 형태로 표출되도록 하기 위해 사용했습니다.

이번에는 새로고침을 해도 목록을 표출하는 페이지와 상세 내용을 표출하는 페이지가 유지되도록 구현하기 위해 query string을 사용하겠습니다.

1️⃣ 구현 계획

페이지의 흐름은 이렇습니다.

  1. 사용자가 목록 페이지에 접속합니다.
  2. 목록 페이지에서 목록을 클릭하여 노션처럼 상세 페이지를 오른쪽에서 왼쪽으로 슬라이드 애니메이션을 사용하여 표출합니다.
  3. 상세 페이지에서 할 일 내용을 수정합니다.
  4. 상세 페이지를 닫습니다. 닫을 때 오른쪽으로 슬라이드 애니메이션을 적용하여 페이지를 닫습니다.

이 상세 페이지는 query string으로 주어진 task 값을 사용하여 내용을 가져와 표출합니다.
페이지가 닫힐 때에는 다시 오른쪽으로 슬라이드 애니메이션을 적용하여 페이지를 닫습니다.

애니메이션 구현은 framer-motion 라이브러리를 사용했습니다.

처음에는 parallel routes를 사용하지 않고 쿼리스트링만 사용해서 하나의 컴포넌트로 구현해도 괜찮을 것 같다고 생각했었습니다.

하지만 하나의 컴포넌트로 구현을 하게 된다면 상세 페이지에서 내용을 수정할 때와 할 일 완료/미완료 버튼으로 인한 상태 변화 등 여러 상태들이 변할 때마다 상세 페이지 뿐만 아니라 목록 페이지도 다시 렌더링 되는 문제가 발생할 것 같았기 때문에 별도의 페이지로 구성하였습니다.

2️⃣ 페이지 구현

할 일 목록 페이지는 /tasklist?list=1과 같은 형태로 접근할 수 있도록 구현했습니다.

그리고 할 일 상세 페이지를 병렬 라우팅으로 표출하기 위해 tasklist 세그먼트에 @task slot을 생성합니다.

typescript
// 구조
📦 src
   └─ app
      └─ [groupId]
         ├─ tasklist
         │  ├─ layout.tsx
         │  ├─ page.tsx
         │  └─ @task
         │     └─ page.tsx
         └─ page.tsx

tasklistlayout.tsx에서 병렬 라우팅으로 표출할 @task slot을 prop으로 받아 return합니다.

typescript
//tasklist/layout.tsx
const Layout = ({
  children,
  task,
}: {
  children: ReactNode;
  task: ReactNode;
}) => {
  return (
    <div className="flex">
      <div className="flex min-h-screen w-full justify-center overflow-auto">
        {children}
      </div>
      {task}
    </div>
  );
};
export default Layout;

목록 상세 페이지는 /tasklist?list=1&task=1과 같은 형태로 접근할 수 있도록 했습니다.
따라서 쿼리스트링의 task 값을 사용해 상세 페이지를 표출합니다.

task 값을 가져오지 못하게 되면 페이지가 닫히게 될텐데 이 때 바로 컴포넌트가 언마운트 되어 애니메이션을 구현할 수 없는 문제가 있었지만 framer motion 라이브러리의 <AnimatePresence /> 컴포넌트를 사용하면 컴포넌트가 언마운트 될 때 애니메이션을 구현할 수 있습니다.

typescript
// @task/page.tsx return문

return (
    <AnimatePresence mode="wait">
      {taskId ? (
        <motion.aside
          key={taskId}
          variants={pageVariants}
          initial="initial"
          animate="visible"
          exit="exit"
        >
          <TaskDetailWrapper onClose={handleClose}>
            <div className="flex flex-col gap-5">
              <div className="flex flex-col gap-10 tablet:gap-14 pc:gap-[68px]">
                <TaskDetailContents
                  {...taskDetailData}
                  createdAt={taskDetailData?.recurring?.createdAt}
                  groupId={groupId}
                  taskListId={taskListId}
                  taskId={taskId}
                  isPending={isPending}
                />
                <div className="flex flex-col gap-4">
                  <div className="flex items-center gap-1">
                    <Icon
                      icon="comment"
                      className="h-[18px] w-[18px] tablet:h-5 tablet:w-5"
                    />
                    <span className="text-lg font-bold text-blue-200">
                      {taskDetailData?.commentCount || 0}
                    </span>
                  </div>
                  <TaskDetailInputReply taskId={taskId} />
                </div>
              </div>
              <TaskDetailComment taskId={taskId} />
            </div>
          </TaskDetailWrapper>
        </motion.aside>
      ) : null}
    </AnimatePresence>
  );

<AnimatePresence /> 컴포넌트가 자식 컴포넌트의 언마운트를 감지하면 애니메이션이 종료될 때 까지 언마운트 시점을 늦춥니다. 따라서 페이지가 사라지는 애니메이션을 전부 표출한 후에 자식 컴포넌트가 언마운트 됩니다.

mode="wait" 옵션을 사용하면 하나의 구성요소만 렌더링 하게 하고 기존 요소의 애니메이션이 종료될 때 까지 기다린 후 다음 요소에 애니메이션을 적용합니다.

적용된 애니메이션입니다.

typescript
const pageVariants = {
  initial: {
    x: '100%',
    opacity: 0,
  },
  visible: {
    x: 0,
    opacity: 1,
    transition: {
      type: 'tween',
      ease: 'easeOut',
      duration: 0.2,
    } as const,
  },
  exit: {
    x: '100%',
    opacity: 0,
    transition: {
      type: 'tween',
      ease: 'easeIn',
      duration: 0.2,
    } as const,
  },
};

처음 상태인 initial로 오른쪽 맨 끝에 위치한 상태입니다.
페이지가 표출될 때 visible 애니메이션이 실행되면서 페이지가 표출되고,
페이지가 닫힐 때 exit 애니메이션이 실행되면서 페이지가 닫히게 됩니다.

master_detail_page

이렇게 parallel routes와 쿼리스트링을 사용한 페이지에 framer-motion 라이브러리로 애니메이션을 적용하여 상세 페이지가 오른쪽에서 튀어나오는 master/detail 형태의 페이지를 제작해보았습니다.

해당 페이지를 구현하면서 next.js의 병렬 라우팅 기능을 응용해보고 framer-motion 라이브러리의 사용법도 간단하게 익힐 수 있었습니다.

Master the Master-Detail Pattern - Windows Blogs
React Animate Presence 개념부터 구현까지 - 냠냠맨

게시글 제목:Next.js Parallel Routes와 query string으로 master/detail 형태의 페이지 구현하기

작성자:huui

게시글 링크:https://huuitae.github.io/posts/nextjs_master_detail_page [복사]

마지막 수정일:


상업적 복제의 경우, 웹마스터에게 허가를 요청하십시오. 비상업적 복제의 경우, 본 기사의 출처와 링크를 명시해 주십시오. 본 저작물은 어떤 형태로든, 어떤 매체로든 자유롭게 복제 및 배포할 수 있으며, 수정 및 제작도 가능합니다. 단, 2차 저작물을 배포할 경우에도 동일한 라이선스 계약을 적용해야 합니다.
이 게시글은 다음을 채택합니다.CC BY-NC-SA 4.0의 허가를 받아야 합니다.