
동적 라우팅 - 상세 페이지 찍어내기
단 하나의 파일만으로 무한대의 페이지를 생성하는 것이 동적 라우팅(Dynamic Routes)입니다.
게시판의 글 번호, 사용자의 ID, 상품의 고유 코드 등은 미리 예측할 수 없는 가변적인 데이터입니다.
이 때, Next.js는 []라는 마법의 문법을 제안합니다.

폴더에 [](대괄호)를 씌우는 순간, 그 폴더는 더 이상 고정된 이름이 아니라
어떤 값이든 받아들이는 와일드카드로 변신하게 됩니다.
백문이 불여일견, 예시로 어떻게 해석되는지 살펴보겠습니다.
우선 app/ 폴더 안에 products/ 라는 폴더를 만듭니다.

그 안에 [slug] 이름이라는 폴더를 생성해보세요.

[] 안에 들어가는 slug는 변수의 이름입니다.
slug는 보통 URL에서 사람이 읽기 편하기 표현된 텍스트를 뜻합니다.
이렇게 설정한다면 SEO 추적에도 유리합니다.
이제 이 slug 값을 받아올텐데, Next.js 15+ 부터 바뀐 점이 있습니다.
URL의 정보를 담는 params가 일반 객체가 아니라 Promise 객체로 변경되었다는 사실입니다.
코드로 확인해봅시다.
export default async function ProductDetailPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return (
<div className="p-18">
<h1 className="text-4xl font-bold text-blue-600">상품 상세 페이지</h1>
<p className="text-xl mt-4 text-gray-700">
고객이 요청하신 상품의 코드는 다음과 같습니다.
<span className="font-bold text-red-500 ml-2 text-3xl">{slug}</span>
</p>
</div>
)
}여기서 slug가 포함된게 Promise 객체이므로, 함수를 async 함수를 만들고 await 키워드를 통해 꺼내옵니다.
브라우저에서 다음과 같이 테스트해봅시다.

결과로는 다음과 같이 맵핑됩니다.

이로써, URL에 입력된 값을 가지고, 데이터베이스에게 해당 slug를 가진 상품을 찾아달라고
명령만 내리면 되는 것입니다. URL 자체가 데이터를 조회하는 queryKey가 되어버립니다.
이를 조금 더 응용해보도록 하겠습니다.
동적 라우팅은 하나만 쓸 수 있는 것이 아닙니다.
만약 어떤 Category에 어떤 상품ID 인지 표현하고 싶다고 해봅시다.

이 때 폴더를 중첩하여 사용하면 됩니다. 그리고 계층 하단에 page.tsx를 배치합니다.

이것을 코드로 표현해봅시다.
export default async function ShopItemPage({
params
}: {
params: Promise<{ category: string, itemId: string }>
}) {
const { category, itemId } = await params
return (
<div className="p-10 border-2 border-emerald-500 rounded-xl m-10">
<h2 className="text-2xl font-bold mb-4">중첩 라우팅 테스트</h2>
<p className="mb-2">
현재 접속하신 카테고리는 {category}이며, 실제 상품의 ID는 {itemId} 입니다.
</p>
</div>
)
}이렇게 만들고, 브라우저 주소창에 다음과 같이 입력합니다.

최종적으로 아래와 같이 맵핑됩니다.

보시다시피, 폴더 이름이 곧 변수 이름이 되기 때문에 각각의 값을 정확히 꺼낼 수 있습니다.
마지막으로, 주의해야 할 점은 동적 라우팅은 강력하지만, 우선순위라는 규칙이 존재합니다.
다음과 같이 동적 경로와 고정 경로가 충돌된다면 어떠할까요?

여기서 사용자가 /..../products/new 라고 접속했을 때, 목적지는 어디가 될까요?
정답은 구체적인 것이 우선하므로, 정적 페이지를 보여줍니다. 따라서 이름이 겹칠 걱정은 하지 않아도 됩니다.
동적 라우팅 - 실전 쇼핑몰 상세페이지 구축
실제로, 웹 사이트에서 위에서 말한 동적 라우팅을 직접 입력해서 들어가는 경우는 없습니다.
따라서, 상품 목록을 보여주는 리스트 페이지를 만들고, 클릭으로 동적 상세 페이지로 이동하는 흐름을 구성해야 합니다.
실습을 위해 임시 데이터(Mock Data)를 구성해야 하며, 그전에 먼저 app/ 안에 shop-products/ 안에 page.tsx를 만듭니다.

그리고 파일 상단에 상품 정보를 담을 배열을 하나 선언합니다.
import Link from "next/link"
const products = [
{ id: 1, name: '초경량 노트북', price: '1,200,000원' },
{ id: 2, name: '무소용 기계식 키보드', price: '185,000원' },
{ id: 3, name: '인체공학 버티컬 마우스', price: '89,000원' }
]
export default function ShopProductListPage() {
return (
<div className="p-10">
<h1 className="text-3xl font-bold mb-8">이달의 추천 상품</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{products.map((product) => (
<div key={product.id}
className="border p-6 shadow-sm">
<h2 className="text-xl font-bold mb-2">{product.name}</h2>
<p className="text-white-600 mb-4">{product.price}</p>
<Link
href={`/shop-products/${product.id}`}
className="inline-block px-4 py-2 rounded hover:bg-blue-700">
상세보기
</Link>
</div>
))}
</div>
</div>
)
}여기서 Link 컴포넌트의 href 속성에 템플릿 리터럴 기호를 사용해 동적 링크로 이동하도록 구성하였습니다.

이제 [id] 폴더를 만들고, 역시 page.tsx 파일을 만들어 코드를 구성합니다.

이제 상세 페이지 코드를 다듬습니다.
const products = [
{ id: 1,
name: '초경량 노트북', price: '1,200,000원',
description: '가방에 넣은 줄도 모르는 가벼움! 대학생 강추 아이템'
},
{ id: 2,
name: '무소음 기계식 키보드', price: '185,000원',
description: '사무실에서도 눈치 보지 않고 타격감을 즐기세요.'
},
{ id: 3,
name: '인체공학 버티컬 마우스',
price: '89,000원',
description: '손목 터널 증후군 예방을 위한 최고의 선택'
}
]
export default async function ProductDetailPage({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params
const product = products.find(p => p.id === parseInt(id))
if (!product) return <div className="p-10 text-red-500">존재하지 않는 상품입니다.</div>
return (
<div className="p-10 max-w-2xl mx-auto">
<div className="bg-white p-8 border border-gray-100">
<span className="bg-blue-100 text-blue-800 text-sm rounded">BEST ITEM</span>
<h1 className="text-4xl text-black font-bold mt-4 mb-2">{product.name}</h1>
<p className="text-2xl text-gray-700 font-bold mb-6">{product.price}</p>
<hr />
<p>{product.description}</p>
</div>
</div>
)
}Search Params
평소에 쇼핑몰을 이용하던 경험을 떠올려보면, 내가 원하는 브랜드 상품을 검색해서 찾아봅니다.
이 때, 웹 브라우저의 주소창에 ?와 함께 붙는 꼬리표를 유심히 보면 다음과 같습니다.

이것을 정식 명칭으로 Search Params 혹은 Query String 이라고 부릅니다.
여기서 개념부터 정리해보겠습니다.
지난 번에 배운 [slug] 주소창이 목적지라면, Search Params는 구체적 옵션과 같습니다.
Next.js에서는 서버 컴포넌트에서 props.searchParams가 전달됩니다.
그리고 15+ 버전에서 꺼내는 방식이 달라졌습니다.
예전 버젼에서는 동기 방식으로 바로 꺼낼 수 있었지만, 15+ 부터는 비동기 방식으로 기다려야 합니다.
이렇게 바뀐 이유는 성능과 랜더링 효율성 때문인데
성능 측면에서 이전 방식에서는 props.searchParams를 전달할 때까지 작업을 멍하니 기다렸습니다.
그러나 변경된 방식에서는 세부 옵션 요청을 고민하는 동안 다른 작업을 진행할 수 있게 되었습니다.
사용자 경험 측면에서는 데이터를 즉시 열어보는 방식은 데이터 처리가 끝날 때까지 화면이 멈추었습니다. (블로킹)
하지만, 비동기를 사용할 경우, 데이터를 기다리는 동안 뼈대(Skeleton)을 먼저 전송하는 스트리밍 방식입니다.
이제 직접 코드를 통해서 살펴봅니다.
다음과 같이 먼저 src/ 안의 app/ 안에 sort-products/를 만들어 page.tsx를 만듭니다.
그리고 상품의 정보를 배열 정보로 가짜 데이터로 만듭니다.
const products = [
{ id: 1, name: '초경량 노트북', price: 1200000 },
{ id: 2, name: '무소음 기계식 키보드', price:185000 },
{ id: 3, name: '인체공학 버티컬 마우스', price: 89000 }
]그리고 PageProps 인터페이스를 정의합니다.
interface PageProps {
searchParams: Promise<{ sort?: string }>
}다음으로 컴포넌트 함수를 작성합니다.
export default async function ProductListPage({ searchParams }: PageProps) {
const { sort } = await searchParams
const sortedProducts = [...products].sort((a, b) => {
if (sort === 'asc') {
return a.price - b.price
} else if (sort === 'desc') {
return b.price - a.price
} else {
return 0
}
})
return (
<div className="p-10">
<div className="flex justify-between items-center mb-8">
<div className="space-x-2 text-black">
<Link
href="/search-products?sort=asc"
className="px-3 py-12 bg-gray-200 rounded text-sm"
>
가격 낮은 순
</Link>
<Link
href="/search-products?sort=desc"
className="px-3 py-12 bg-gray-200 rounded text-sm"
>
가격 높은 순
</Link>
</div>
<div className="grid gird-cols-1 gap-6">
{sortedProducts.map((product) => (
<div
key={product.id}
className="border rounded-lg p-6 shadow-sm"
>
<h2 className="text-xl font-bold mb-2">{product.name}</h2>
<p className="text-gray-600 text-white mb-4">
{product.price.toLocaleString()}원
</p>
<Link
href={`/search-products/${product.id}`}
className="inline-block bg-blue-600 text-white px-4 py-2"
>
상세보기
</Link>
</div>
))}
</div>
</div>
</div>
)
}여기서 이해해야 할 점은 URL이 바뀌면, 서버 컴포넌트가 다시 실행된다는 점입니다.
버튼을 눌러서 주소를 바꾸는 순간, 주소창에 sort라는 값이 바뀌었음을 감지합니다.
그리고 서버 컴포넌트 함수를 처음부터 다시 실행합니다.
그때 비로소, await searchParams 함수가 다시 실행되면서 새로운 sort 값을 가져오고
그에 맞춰 정렬 순서가 바뀐 새로운 HTML을 만들어서 우리에게 보여주는 것입니다.

이제 응용된 방식으로, Next.js의 서버 컴포넌트를 이용해