[이제와서 시작하는 Next.js 마스터하기 #7] 동적 라우팅과 고급 패턴
[이제와서 시작하는 Next.js 마스터하기 #7] 동적 라우팅과 고급 패턴
“라우팅이 이렇게 강력할 수 있다니!” - Parallel Routes와 Intercepting Routes로 복잡한 UI도 쉽게!
🎯 이 글에서 배울 내용
- Parallel Routes (병렬 라우트)
- Intercepting Routes (가로채기 라우트)
- Route Handlers (API Routes)
- Middleware 기본
예상 소요 시간: 45분
🔀 Parallel Routes - 동시에 여러 페이지 표시
1. 기본 개념
1
2
3
4
5
6
7
app/
├── layout.js
├── page.js
├── @sidebar/
│ └── page.js
└── @main/
└── page.js
1
2
3
4
5
6
7
8
9
// app/layout.js
export default function Layout({ children, sidebar, main }) {
return (
<div className="grid grid-cols-12">
<aside className="col-span-3">{sidebar}</aside>
<main className="col-span-9">{main}</main>
</div>
);
}
2. 실전 예제: 대시보드
1
2
3
4
5
6
7
8
app/dashboard/
├── layout.js
├── @analytics/
│ └── page.js // 분석 패널
├── @revenue/
│ └── page.js // 매출 패널
└── @users/
└── page.js // 사용자 패널
1
2
3
4
5
6
7
8
9
10
// app/dashboard/layout.js
export default function DashboardLayout({ analytics, revenue, users }) {
return (
<div className="grid grid-cols-3 gap-4">
<div>{analytics}</div>
<div>{revenue}</div>
<div>{users}</div>
</div>
);
}
🎭 Intercepting Routes - 모달 패턴
1. 사진 갤러리 예제
1
2
3
4
5
6
7
8
9
app/
├── photos/
│ ├── page.js // /photos
│ └── [id]/
│ └── page.js // /photos/1
└── @modal/
└── (..)photos/
└── [id]/
└── page.js // 모달로 표시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/photos/page.js
export default function PhotosPage() {
const photos = [1, 2, 3, 4];
return (
<div className="grid grid-cols-4 gap-4">
{photos.map(id => (
<Link key={id} href={`/photos/${id}`}>
<img src={`/photo-${id}.jpg`} alt={`Photo ${id}`} />
</Link>
))}
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
// app/@modal/(..)photos/[id]/page.js
import Modal from '@/components/Modal';
export default async function PhotoModal({ params }) {
const { id } = await params;
return (
<Modal>
<img src={`/photo-${id}.jpg`} alt={`Photo ${id}`} />
</Modal>
);
}
2. Modal 컴포넌트 구현
위 예제에서 사용한 Modal 컴포넌트를 직접 만들어보겠습니다!
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// components/Modal.js
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';
export default function Modal({ children }) {
const router = useRouter();
const dialogRef = useRef(null);
useEffect(() => {
// 모달을 자동으로 열기
if (dialogRef.current) {
dialogRef.current.showModal();
}
}, []);
function onDismiss() {
router.back(); // 이전 페이지로 돌아가기
}
function onClickOutside(e) {
// 모달 외부 클릭 시 닫기
if (e.target === dialogRef.current) {
onDismiss();
}
}
return (
<dialog
ref={dialogRef}
onClose={onDismiss}
onClick={onClickOutside}
className="backdrop:bg-black backdrop:opacity-50 p-0 rounded-lg shadow-2xl"
>
<div className="relative">
{/* 닫기 버튼 */}
<button
onClick={onDismiss}
className="absolute top-4 right-4 text-white bg-black/50 rounded-full w-8 h-8 flex items-center justify-center hover:bg-black/70"
>
✕
</button>
{/* 모달 내용 */}
<div className="p-6">
{children}
</div>
</div>
</dialog>
);
}
코드 설명 (초보자용):
<dialog>태그: HTML5의 네이티브 모달 요소showModal(): 모달을 화면 중앙에 표시backdrop: 모달 뒤의 어두운 배경
useRouter(): Next.js 라우터 사용router.back(): 이전 페이지로 돌아가기- 모달을 닫으면 갤러리로 돌아감
- 외부 클릭 감지:
onClick={onClickOutside}: 모달 외부 클릭 시 닫기e.target === dialogRef.current: 배경 클릭인지 확인
💡 Tip: 이 Modal 컴포넌트는 다른 곳에서도 재사용 가능합니다!
1
2
3
4
5
// 다른 곳에서 사용
<Modal>
<h2>알림</h2>
<p>저장되었습니다!</p>
</Modal>
🛣️ Route Handlers - API 엔드포인트
1. GET 요청
1
2
3
4
5
6
7
8
9
10
11
12
// app/api/posts/route.js
export async function GET(request) {
// 실제로는 데이터베이스에서 가져옵니다
// 예: const posts = await prisma.post.findMany();
// 여기서는 외부 API로 시뮬레이션
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await response.json();
// 최근 10개만 반환
return Response.json(posts.slice(0, 10));
}
사용 예시:
1
2
3
4
// 클라이언트에서 호출
const response = await fetch('/api/posts');
const posts = await response.json();
console.log(posts); // 게시물 목록 출력
2. POST 요청
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
// app/api/posts/route.js
export async function POST(request) {
const data = await request.json();
// 유효성 검사
if (!data.title || !data.content) {
return Response.json(
{ error: '제목과 내용은 필수입니다' },
{ status: 400 }
);
}
// 실제로는 데이터베이스에 저장
// 예: const post = await prisma.post.create({ data: { title: data.title, content: data.content } });
// 시뮬레이션: 새 게시물 객체 생성
const newPost = {
id: Date.now(),
title: data.title,
content: data.content,
createdAt: new Date().toISOString()
};
console.log('📝 새 게시물 생성:', newPost);
return Response.json(newPost, { status: 201 });
}
사용 예시:
1
2
3
4
5
6
7
8
9
10
11
12
// 클라이언트에서 호출
const response = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: '새 게시물',
content: '내용입니다'
})
});
const newPost = await response.json();
console.log('생성됨:', newPost);
3. 동적 라우트
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
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
const { id } = await params;
// 실제로는 데이터베이스에서 조회
// 예: const post = await prisma.post.findUnique({ where: { id: parseInt(id) } });
// 외부 API로 시뮬레이션
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) {
return Response.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
const post = await response.json();
return Response.json(post);
}
export async function DELETE(request, { params }) {
const { id } = await params;
// 실제로는 데이터베이스에서 삭제
// 예: await prisma.post.delete({ where: { id: parseInt(id) } });
console.log(`🗑️ 게시물 ${id} 삭제`);
return Response.json({ success: true, message: `Post ${id} deleted` });
}
사용 예시:
1
2
3
4
5
6
7
8
9
10
11
// GET: 특정 게시물 조회
const response = await fetch('/api/posts/1');
const post = await response.json();
console.log(post);
// DELETE: 게시물 삭제
const deleteResponse = await fetch('/api/posts/1', {
method: 'DELETE'
});
const result = await deleteResponse.json();
console.log(result); // { success: true, message: 'Post 1 deleted' }
🔍 자주 묻는 질문 (FAQ)
Q1: Parallel Routes는 언제 사용하나요?
사용 케이스:
- 대시보드 (여러 패널 동시 표시)
- 소셜 미디어 (피드 + 사이드바)
- 이커머스 (상품 목록 + 필터)
장점:
- 각 섹션 독립적으로 로딩
- 에러 격리
- 병렬 데이터 페칭
Q2: Intercepting Routes는 언제 쓰나요?
사용 케이스:
- 갤러리 모달
- 로그인/회원가입 모달
- 빠른 미리보기
장점:
- 부드러운 UX
- URL 변경은 되지만 전체 페이지는 안 바뀜
- 뒤로가기 지원
🎯 오늘 배운 내용 정리
- Parallel Routes
- @folder 문법
- 여러 페이지 동시 표시
- 독립적인 로딩/에러 처리
- Intercepting Routes
- (.)folder 문법
- 모달 패턴
- 부드러운 UX
- Route Handlers
- RESTful API 생성
- GET/POST/DELETE 등
- 동적 라우트 지원
📚 시리즈 네비게이션
“고급 라우팅 패턴으로 프로답게!” 🎨
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.