Next.js Parallel Routes의 slot과 default.tsx는 언제 사용되나요?
Parallel Routes의 @panel 같은 폴더 이름은 URL에 붙지 않습니다. layout이 같은 이름의 prop을 받아 본문과 나란히 렌더링하며, 직접 접근에서 slot 상태를 복원할 수 없으면 default.tsx가 기본 화면을 제공합니다.
적용 조건·범위: 전체 페이지 로드에서 현재 URL에 맞는 slot의 활성 상태를 복원할 수 없을 때 기본 UI를 제공합니다. 클라이언트 이동에서 이전 slot 상태가 유지되는 경우와 구분합니다.
독립 실습: 이름 있는 slot과 기본 화면 확인하기
React 컴포넌트·TypeScript·async/await를 알고 시작합니다. Next.js 환경 준비의 루트 app/ TypeScript App Router 프로젝트를 별도 연습 폴더에서 사용합니다. 이 글의 기존 실습 기준은 Next.js 16.3.5이며 Cache Components는 끈 구성입니다. 생성된 app/layout.tsx는 유지합니다. 아래 파일은 TaskFlow·인증·DB 없이 실행하는 독립 예제입니다.
npm run dev 후 /practice를 주소창에서 직접 엽니다. slot 이름 panel과 layout prop을 일치시킵니다.
app/practice/layout.tsx 전체 코드
import type { ReactNode } from 'react';
export default function Layout({ children, panel }: { children: ReactNode; panel: ReactNode }) {
return <main>{children}<aside>{panel}</aside></main>;
}
app/practice/page.tsx 전체 코드
export default function Page() { return <h1>본문</h1>; }
app/practice/@panel/default.tsx 전체 코드
export default function DefaultPanel() { return <p>기본 패널</p>; }
기대 결과: 본문과 기본 패널이 함께 표시됩니다. @panel은 URL segment가 아닙니다.
자가 확인: default.tsx가 없는 상태의 직접 새로고침과 차이를 별도 복사본에서 비교하고 기본 파일을 복구합니다.
기본 구조
app/(app)/
├─ layout.tsx
└─ @modal/
└─ default.tsx
@modal은 URL segment가 아닙니다.
layout에서 slot 받기
import type { ReactNode } from 'react';
export default function AppLayout({
children,
modal,
}: {
children: ReactNode;
modal: ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}
default.tsx
export default function DefaultModal() {
return null;
}
전체 페이지 로드에서 현재 URL에 맞는 slot의 활성 상태를 복원할 수 없을 때 기본 UI를 제공합니다. 클라이언트 이동에서 이전 slot 상태가 유지되는 경우와 구분합니다.
자주 발생하는 오류
- @modal을 URL 경로로 오해
- default.tsx 누락
- slot 이름과 layout props 이름 불일치
- 새로고침 시 slot 매칭 차이를 고려하지 않음
클라이언트 이동은 다른 slot의 기존 활성 상태를 유지할 수 있습니다. 전체 새로고침은 그 상태를 복원할 수 없는 slot에 default.tsx를 사용하며 기본 파일도 없으면 404가 될 수 있습니다. 따라서 default.tsx를 모든 이동에서 자동으로 실행되는 페이지로 이해하지 않습니다.
원리 확인: Next.js Parallel Routes 기본 상태·탐색 (2026-10-12). 아래는 기대 결과이며 이 편집에서 실제 실행은 확인하지 않았습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 Next.js로 페이지와 서버 기능을 확장합니다.
기존 SiteHeader를 유지하면서 layout에 modal slot을 추가합니다. default.tsx와 catch-all의 null 페이지는 빈 slot과 모달 해제를 담당합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow Next.js 프로젝트 적용
시작 체크포인트: 이전: Next.js Route Group Private Folder template tsx의 완료 상태에서 이어갑니다.
이전 회차 완료 ZIP으로 시작하기. 새 폴더에 풀어 현재 회차 완료본과 나란히 비교합니다.
이전 완료본과 달라진 파일: app/(app)/@modal/[...catchAll]/page.tsx, app/(app)/@modal/default.tsx, app/(app)/layout.tsx, package.json. 아래 변경 설명을 따라 수정하고 전체 코드의 같은 파일과 대조합니다.
직접 확인: 빈 modal slot의 화면
독자 질문: @modal 폴더를 만들면 지금 모달이 뜰까요? 완료 코드의 default.tsx와 [...catchAll]/page.tsx는 모두 null을 반환합니다. 7회차 화면에 모달이 없는 것은 예상 결과입니다.
app/(app)/layout.tsx가children과modal을 모두 렌더링하는지 확인합니다.@modal은 URL 경로가 아닙니다. 빈 상태의 두 파일을 비교합니다.npm run dev뒤/tasks의 ‘작업 목록’·검색 입력·작업 상태와/settings의 ‘설정’을 확인합니다. 공통 헤더는 보이고 modal slot의 내용은 비어 있어야 합니다. 주소 직접 입력과 새로고침도 확인합니다.- slot 이름과 layout의
modalprop 이름이 일치하는지 확인합니다.default.tsx가 빠졌다면 새로고침이나 일치하지 않는 slot의 404를 점검합니다. npm run typecheck와npm run build의 종료 코드를 각각 기록합니다.
검증 상태: 이번 편집에서 개발 서버·빌드·브라우저를 실행하지 않아 실제 화면과 오류 부재는 미확인입니다.
프로젝트 완료 기준
/tasks의 본문과 공통 헤더가 표시되고 빈 modal slot이 다른 화면을 가리지 않아야 합니다. slot을 준비하는 이번 회차와 내용을 채우는 다음 회차의 목표를 구분합니다.
완료 코드 ZIP 다운로드 — next-step-07-parallel-routes
이전 체크포인트: next-step-06-route-groups
이번 회차의 변경
기존 SiteHeader를 유지하면서 layout에 modal slot을 추가합니다. default.tsx와 catch-all의 null 페이지는 빈 slot과 모달 해제를 담당합니다.
변경·추가 파일:
app/(app)/layout.tsxapp/(app)/@modal/default.tsxapp/(app)/@modal/[...catchAll]/page.tsx
전체 파일·코드 보기
실행과 검증
Node.js 22 LTS 이상 권장(Next.js 최소 20.9). 루트 app/ 구조, CSS 사용.
npm install
npm run dev
npm run build
npm run start
전체 코드와 ZIP은 같은 완료 체크포인트입니다. 자동 생성 worker는 ZIP에 포함하며 라이브러리 파일은 코드 뷰어에서 생략합니다.
완료 체크포인트: next-step-07-parallel-routes
실습 프로젝트 전체 코드
.gitignore
node_modules
.next
.env*
!.env.example
*.tsbuildinfo
app/(app)/@modal/[…catchAll]/page.tsx
export default function ResetModal() {
return null;
}
app/(app)/@modal/default.tsx
export default function Default() {
return null;
}
app/(app)/layout.tsx
import type { ReactNode } from "react";
import SiteHeader from "@/app/_components/SiteHeader";
export default async function AppLayout({
children,
modal,
}: {
children: ReactNode;
modal: ReactNode;
}) {
return (
<>
<SiteHeader />
{children}
{modal}
</>
);
}
app/(app)/settings/page.tsx
export default function SettingsPage() {
return (
<main>
<h1>설정</h1>
<p>TaskFlow 설정 화면입니다.</p>
</main>
);
}
app/(app)/tasks/page.tsx
import Link from "next/link";
import { getTasks } from "@/app/lib/data";
import TaskSearch from "@/app/ui/task-search";
export default async function TasksPage() {
const tasks = await getTasks();
return (
<main>
<h1>작업 목록</h1>
<TaskSearch />
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title}
<span> · {task.completed ? "완료" : "진행 중"}</span>
</li>
))}
</ul>
</main>
);
}
app/(app)/template.tsx
import type { ReactNode } from "react";
export default function Template({ children }: { children: ReactNode }) {
return <>{children}</>;
}
app/(public)/page.tsx
import Link from "next/link";
export default function HomePage() {
return (
<main>
<h1>TaskFlow Next.js</h1>
<p>App Router 기반 작업 관리 서비스입니다.</p>
<Link href="/tasks">작업 목록 보기</Link>
</main>
);
}
app/_components/SiteHeader.tsx
import Link from "next/link";
export default function SiteHeader() {
return (
<header>
<nav aria-label="주 메뉴">
<Link href="/">홈</Link>
<Link href="/tasks">작업</Link>
<Link href="/settings">설정</Link>
</nav>
</header>
);
}
app/globals.css
* {
box-sizing: border-box;
}
body {
margin: 0;
color: #171717;
background: #fafafa;
font-family: Arial, sans-serif;
line-height: 1.6;
}
header,
main {
width: min(880px, calc(100% - 32px));
margin: 24px auto;
}
nav {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
a {
color: inherit;
text-underline-offset: 3px;
}
button,
input,
select {
min-height: 44px;
padding: 8px 12px;
font: inherit;
border: 1px solid #aaa;
background: #fff;
color: #171717;
max-width: 100%;
}
button {
cursor: pointer;
}
button:disabled {
opacity: 0.6;
cursor: wait;
}
input {
display: block;
width: 100%;
}
label {
display: block;
}
form,
section,
aside {
margin: 20px 0;
}
ul {
padding: 0;
list-style: none;
}
li {
padding: 12px;
margin: 12px 0;
border: 1px solid #ccc;
overflow-wrap: anywhere;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.controls form {
margin: 0;
}
.modal {
width: min(640px, calc(100% - 32px));
border: 1px solid #777;
padding: 24px;
background: #fff;
color: #171717;
}
.modal::backdrop {
background: rgb(0 0 0 / 55%);
}
:focus-visible {
outline: 3px solid #555;
outline-offset: 3px;
}
[data-theme="dark"] {
background: #202020;
color: #fafafa;
padding: 16px;
}
[data-density="compact"] li {
padding: 8px;
margin: 8px 0;
}
img {
max-width: 100%;
height: auto;
}
app/layout.tsx
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
export const metadata: Metadata = {
title: { default: "TaskFlow Next.js", template: "%s | TaskFlow" },
description: "서버와 브라우저 역할을 배우는 TaskFlow 학습 프로젝트",
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}
app/lib/data.ts
import type { Task } from "./types";
const tasks: Task[] = [
{
id: "task-1",
title: "App Router 공부",
description: "서버와 브라우저의 역할을 구분합니다.",
completed: false,
ownerId: "demo",
},
{
id: "task-2",
title: "TaskFlow 만들기",
description: "누적 실습을 완성합니다.",
completed: true,
ownerId: "demo",
},
];
export async function getTasks() {
return tasks;
}
export async function getTask(id: string) {
return tasks.find((t) => t.id === id);
}
export async function getTaskStats() {
const ts = await getTasks();
return { total: ts.length, completed: ts.filter((t) => t.completed).length };
}
app/lib/types.ts
export type Task = {
id: string;
title: string;
description: string;
completed: boolean;
ownerId: string;
};
app/ui/task-search.tsx
"use client";
import { useState } from "react";
export default function TaskSearch() {
const [keyword, setKeyword] = useState("");
return (
<label>
작업 검색
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="검색어 입력"
/>
</label>
);
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = { cacheComponents: false };
export default config;
package.json
{
"name": "taskflow-next",
"version": "1.0.7",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "16.3.5",
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"typescript": "5.9.3",
"@types/node": "24.13.6",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
글 소개·학습 준비·전체 목차

Next.js · 7/38 · TaskFlow 누적 실습
Next.js Parallel Routes default tsx
Next.js · 7 / 38 · 필수 학습 · Parallel Routes·default.tsx
Parallel slot은 어떤 URL을 만들고 default.tsx는 언제 필요할까요? @panel 같은 이름은 URL에 추가되지 않으며 layout의 같은 이름 prop으로 렌더링합니다. 직접 접근에서 맞는 slot 상태를 복원할 수 없으면 default.tsx가 기본 UI를 제공합니다. 본문과 기본 패널을 함께 띄워 이 계약을 확인합니다.
이번 글에서 배우는 내용
- slot 개념
- @folder
- layout props
- default.tsx
- soft navigation과 hard navigation 차이
목차
다음 학습
다음 글에서는 Parallel Routes와 Intercepting Routes를 결합해 URL은 상세 주소로 바뀌면서 목록 위에 상세 모달을 띄우는 패턴을 만듭니다.
공식 문서
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 39개 · 필수 38개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.