page 파일은 특정 라우트(경로)에 고유한 UI를 정의할 수 있게 해주는 아주 중요한 파일입니다. 파일에서 React 컴포넌트를 기본(default)으로 내보내기(export)만 하면 해당 경로의 페이지를 만들 수 있어요. 아주 간단하죠?
💡 강사의 팁! > Next.js의 App Router에서는 폴더 이름이 곧 URL 경로가 됩니다. 하지만 폴더만 만든다고 화면에 보이는 건 아니에요. 화면에 무언가를 띄우려면 반드시 그 폴더 안에
page.js(또는.tsx) 파일이 있어야 한답니다!
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
return <h1>My Page</h1>
}
export default function Page({ params, searchParams }) {
return <h1>My Page</h1>
}
page.js를 사용하시기 전에 꼭 기억해두셔야 할 몇 가지 특징들이 있어요.
page 파일에는 .js, .jsx 또는 .tsx 파일 확장자를 모두 사용할 수 있습니다. TypeScript를 쓰신다면 .tsx를 주로 쓰시게 될 거예요!page는 항상 라우트 하위 트리(route subtree)의 리프(leaf, 가장 끝단) 노드 역할을 합니다. (위 그림을 참고하시면 이해가 더 쉬울 거예요!)page 파일이 필요합니다. 'use client'를 적어주면 클라이언트 컴포넌트(Client Component)로 설정할 수도 있답니다.💡 강사의 보충 설명:
"서버 컴포넌트가 기본이다"라는 말이 아주 중요합니다!page.js는 기본적으로 서버에서 미리 렌더링된 HTML을 내려보내주기 때문에 검색엔진 최적화(SEO)에 매우 유리해요. 상태(state) 변화나 사용자 이벤트(클릭 등)가 필요한 페이지가 아니라면 가급적 서버 컴포넌트를 유지하는 것이 성능에 좋습니다.
이제 page.js 컴포넌트가 전달받는 props에 대해 자세히 알아볼까요?
params (선택 사항)params는 최상위 루트 세그먼트부터 해당 페이지까지의 동적 라우트 매개변수(dynamic route parameters)를 포함하는 객체로 반환되는(resolve) Promise입니다.
말이 조금 어려울 수 있는데, URL 경로 중에 [slug] 처럼 대괄호로 묶인 부분의 값을 읽어올 때 사용하는 거예요.
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
}
export default async function Page({ params }) {
const { slug } = await params
}
| 예시 라우트 (Example Route) | URL 형태 (URL) | 전달되는 params 값 |
|---|---|---|
app/shop/[slug]/page.js | /shop/1 | Promise<{ slug: '1' }> |
app/shop/[category]/[item]/page.js | /shop/1/2 | Promise<{ category: '1', item: '2' }> |
app/shop/[...slug]/page.js | /shop/1/2 | Promise<{ slug: ['1', '2'] }> |
params prop은 Promise 형태이기 때문에, 값을 사용하려면 반드시 async/await를 사용하거나 React의 use 함수를 이용해야 합니다.params가 동기식(synchronous) prop이었습니다. 하위 호환성을 위해 Next.js 15에서도 여전히 동기식으로 접근할 수는 있지만, 이 동작 방식은 앞으로 폐기될(deprecated) 예정이에요.💡 강사의 팁! (Next.js 15 업데이트)
왜 Promise로 바뀌었을까요? Next.js가 데이터를 더 효율적으로 가져오고, 부분 렌더링(Partial Prerendering) 같은 최적화 기술을 완벽하게 지원하기 위해 모든 요청 관련 데이터를 비동기로 처리하도록 설계를 바꿨기 때문이에요. 앞으로는await params를 쓰는 습관을 들이는 것이 좋습니다!
searchParams (선택 사항)searchParams는 현재 URL의 검색 매개변수(search parameters, 쿼리 스트링)를 포함하는 객체로 반환되는 Promise입니다. ? 뒤에 붙는 값들을 의미해요. 예를 들면:
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters
}
export default async function Page({ searchParams }) {
const filters = (await searchParams).filters
}
클라이언트 컴포넌트(Client Component) 페이지에서도 React의 use 훅(hook)을 사용해서 searchParams에 접근할 수 있습니다.
'use client'
import { use } from 'react'
export default function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = use(searchParams).filters
}
'use client'
import { use } from 'react'
export default function Page({ searchParams }) {
const filters = use(searchParams).filters
}
| 예시 URL (Example URL) | 반환되는 searchParams 값 |
|---|---|
/shop?a=1 | Promise<{ a: '1' }> |
/shop?a=1&b=2 | Promise<{ a: '1', b: '2' }> |
/shop?a=1&a=2 | Promise<{ a: ['1', '2'] }> |
searchParams prop 역시 Promise입니다. 따라서 값을 꺼내 쓰려면 async/await나 React의 use 함수를 사용해야 합니다.params와 마찬가지로 버전 14 및 그 이전에는 동기식이었지만, Next.js 15에서도 호환성을 위해 당분간은 동기식 접근이 가능합니다. 단, 곧 폐기(deprecated)됩니다.searchParams는 사전에 값을 미리 알 수 없는 동적 API(Dynamic API) 입니다. 이 값을 사용하게 되면 해당 페이지는 사용자가 요청하는 시점에 렌더링을 수행하는 동적 렌더링(dynamic rendering) 방식으로 동작하게 됩니다.searchParams는 브라우저의 URLSearchParams 인스턴스가 아니라, 평범한 JavaScript 객체(plain JavaScript object) 형태로 들어온다는 점을 주의하세요.💡 강사의 꿀팁!
"동적 렌더링"으로 바뀐다는 부분, 아주 무서운(?!) 이야기입니다. 만약 여러분의 페이지가 엄청나게 빠른 정적 페이지(Static Page)로 캐싱되길 원한다면, 서버 컴포넌트에서searchParams를 읽는 것은 피하는 게 좋아요.searchParams를 읽는 순간 "아, 이 페이지는 요청마다 값이 달라질 수 있구나!" 하고 캐싱을 포기해버리거든요.
TypeScript를 사용하신다면, 라우트 리터럴(경로 문자열)로부터 강력하게 타입이 지정된(strongly typed) params와 searchParams를 얻기 위해 페이지에 PageProps 타입을 지정할 수 있습니다. PageProps는 전역(global)으로 사용할 수 있는 헬퍼 타입이에요.
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
const query = await props.searchParams
return <h1>Blog Post: {slug}</h1>
}
알아두면 좋은 점 (Good to know)
- 리터럴 라우트(예:
'/blog/[slug]')를 사용하면, 자동 완성(autocomplete) 기능과 함께params에 대한 엄격한 키(strict keys) 검사가 활성화됩니다. 오타를 방지할 수 있죠!- 정적 라우트(동적 파라미터가 없는 경로)의 경우
params는 빈 객체{}로 해석됩니다.- 이 타입들은
next dev(개발 서버 실행),next build(빌드), 또는next typegen명령어 실행 시에 자동으로 생성됩니다.- 타입이 한 번 생성되고 나면,
PageProps헬퍼는 전역적으로 사용할 수 있습니다. 따로import구문을 써서 가져올 필요가 없어요.
실제로 코드를 어떻게 작성하는지 예제를 통해 감을 잡아보도록 해요.
params를 기반으로 콘텐츠 표시하기동적 라우트 세그먼트(dynamic route segments)를 사용하면, params prop의 값을 기반으로 해당 페이지에 특정 콘텐츠를 보여주거나 서버에서 데이터를 가져올(fetch) 수 있습니다.
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>Blog Post: {slug}</h1>
}
export default async function Page({ params }) {
const { slug } = await params
return <h1>Blog Post: {slug}</h1>
}
💡 강사의 추가 설명: 보통 이런 패턴은 블로그 상세 페이지나 상품 상세 페이지에서
await params로 글 ID나 상품 ID(slug)를 빼낸 뒤에,const data = await fetchPost(slug)처럼 데이터베이스나 API 서버에서 정보를 불러올 때 많이 쓴답니다!
searchParams를 활용한 필터링 처리searchParams prop을 사용하면 URL의 쿼리 스트링(query string)을 기반으로 필터링, 페이지네이션(pagination, 페이징), 정렬 등의 기능을 구현할 수 있습니다. 게시판이나 쇼핑몰 검색 결과에서 자주 쓰는 기능이죠.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { page = '1', sort = 'asc', query = '' } = await searchParams
return (
<div>
<h1>Product Listing</h1>
<p>Search query: {query}</p>
<p>Current page: {page}</p>
<p>Sort order: {sort}</p>
</div>
)
}
export default async function Page({ searchParams }) {
const { page = '1', sort = 'asc', query = '' } = await searchParams
return (
<div>
<h1>Product Listing</h1>
<p>Search query: {query}</p>
<p>Current page: {page}</p>
<p>Sort order: {sort}</p>
</div>
)
}
searchParams 및 params 읽기클라이언트 컴포넌트(Client Component)는 async 함수로 선언할 수 없습니다. 따라서 클라이언트 컴포넌트 내에서 Promise 형태인 searchParams와 params를 사용하려면, React의 use 훅을 사용하여 Promise 값을 읽어내야 해요.
'use client'
import { use } from 'react'
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { slug } = use(params)
const { query } = use(searchParams)
}
'use client'
import { use } from 'react'
export default function Page({ params, searchParams }) {
const { slug } = use(params)
const { query } = use(searchParams)
}
💡 강사의 팁!
use함수는 React 19에서 새롭게 도입된 기능이에요. 컴포넌트 내부에서 Promise가 해결(resolve)될 때까지 기다릴 수 있게 해주는 아주 마법 같은 함수랍니다. 클라이언트 컴포넌트에서는await대신use()를 쓴다는 점, 잊지 마세요!
이 기능들이 언제 생겼고, 언제 변경되었는지 살펴볼까요?
| 버전 (Version) | 변경 사항 (Changes) |
|---|---|
v15.0.0-RC | params와 searchParams가 이제 Promise 형태로 변경되었습니다. 업그레이드를 돕기 위한 코드 변경 도구(codemod)를 사용할 수 있습니다. |
v13.0.0 | page 파일 규칙이 처음 도입되었습니다. |
모든 문서의 구조적(semantic) 개요를 보려면 https://nextjs.org/docs/sitemap.md를 확인하세요.
모든 사용 가능한 문서의 색인(index)을 보려면 https://nextjs.org/docs/llms.txt를 확인하세요.
수고하셨습니다! page.js는 Next.js의 가장 기본이 되는 단위니까 오늘 배운 내용을 꼭 기억해주세요. 추가로 App Router의 다른 파일 규칙(예: layout.js 또는 loading.js)에 대해서도 이어서 알아볼까요?