page.js 파일 이해하기
이번에는 Next.js에서 route(경로)마다 고유한 UI를 정의할 수 있는 page.js
파일에 대해 이야기해볼게요. 간단히 말해서, 특정 경로에 연결되는 페이지 컴포넌트를 만들고 싶으면, 해당 경로 폴더 안에 page.js
파일을 만들고 기본 내보내기(default export)로 컴포넌트를 작성하면 됩니다.
예를 들어, 이렇게 작성할 수 있어요:
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
return <h1>My Page</h1>
}
- params: URL 경로의 동적 세그먼트(예:
/post/[slug]
에서slug
값)를 받을 때 사용돼요. - searchParams: 쿼리 스트링에 포함된 키-값 쌍을 객체 형태로 받을 수 있어요.
하지만 위 예제에서는 params와 searchParams 타입을 Promise
로 감싸고 있는데, 이는 Next.js 13+에서 서버 컴포넌트가 비동기 작업을 할 수 있어서 그런 경우가 많아요.
여기서 알아두면 좋은 점들
내용 | 설명 |
---|---|
기본 내보내기 필수 | page.js 에 기본으로 export된 React 컴포넌트가 있어야 Next.js가 해당 페이지를 인식해요. |
서버 컴포넌트 기본 적용 | Next.js 13부터는 기본적으로 page.js 는 서버 컴포넌트라서, 클라이언트 상태 관리가 필요하면 별도 설정 필요해요.(예: 'use client' 선언) |
동적 라우팅 지원 | params 를 통해 동적 경로 세그먼트를 쉽게 받아올 수 있어 동적인 페이지 구현이 편리해졌어요. |
쿼리 파라미터 사용법 | searchParams 로 URL에 전달된 쿼리 파라미터를 받을 수 있어, 필터링이나 검색 기능 구현 가능해요. |
추가 팁!
-
'use client'
선언
만약 해당page.js
안에서useState
,useEffect
같은 클라이언트 전용 API를 사용한다면, 파일 최상단에'use client'
를 꼭 작성해 줘야 해요. 그래야 Next.js가 클라이언트 컴포넌트로 취급합니다. -
비동기 컴포넌트
위 코드처럼 파라미터가Promise
로 되어 있다면, 컴포넌트를async
함수로 만들어서 데이터를 서버에서 직접 fetch하거나 처리할 수도 있어요. -
파일 위치에 따른 라우팅
app
디렉터리 구조에 따라서 자동으로 URL 경로가 결정되니까, 폴더 이름과 파일 이름이 곧 URL이 된다고 생각하면 편합니다.
page.js
를 활용하면 각 경로별로 독립적인 UI 및 데이터 처리가 매우 편리해져서, Next.js의 강력한 기능을 실제 프로젝트에 잘 녹여내는 데 큰 도움이 됩니다!
한번 직접 만들어 보면서 익혀보시길 추천드려요 :)
이번 글에서는 Next.js에서 페이지 파일과 라우팅에 대해 간단하게 정리해볼게요. 주로 .js
, .jsx
, .tsx
확장자를 가진 파일들이 페이지로서 사용되는데, 이 부분부터 시작해봅시다.
Next.js 페이지 파일 정리
-
페이지 확장자
.js
,.jsx
,.tsx
확장자가 페이지 파일로 사용됩니다.
즉, 이 파일들을pages
디렉토리나 앱 디렉토리(route segment) 안에 넣으면 해당 파일이 하나의 페이지로 처리돼요. -
페이지가 항상 라우트 트리의 '리프(leaf)'이다
이 말은, 페이지가 최종적으로 도착하는 경로의 끝부분(리프 노드)이란 뜻입니다.
중간 경로(segment)는 레이아웃이나 다른 로직을 담당하고, 실제 화면에 그려지는 페이지는 트리의 가장 끝에 있어요. -
라우트 세그먼트를 공개하려면 페이지 파일이 필수!
어떤 경로(segment)를 외부에서 접근 가능하게 만들려면 해당 위치에 페이지 파일이 꼭 필요합니다.
페이지 파일이 없다면 그 경로는 사용자에게 보여지지 않아요. -
페이지는 기본적으로 서버 컴포넌트(Server Components)
Next.js 13부터는 페이지가 서버 컴포넌트로 기본 설정되어 있어 서버에서 렌더링됩니다.
하지만 필요에 따라 클라이언트 컴포넌트(Client Component)로 설정할 수도 있어, 인터랙티브한 UI가 필요할 때는 클라이언트 컴포넌트로 바꿔주면 됩니다.
참고: Props 중 params
페이지 컴포넌트에 전달되는 params
는 선택 사항이며, 동적 라우팅에 많이 사용됩니다.
URL 경로에서 동적 세그먼트를 추출해 매개변수로 전달해주기 때문에, 예를 들어 블로그 글 ID 같은 값을 받아 처리할 때 유용하죠.
추가로 알아두면 좋은 점들
-
서버 컴포넌트 vs 클라이언트 컴포넌트
- 서버 컴포넌트는 초기 렌더링 속도가 빠르고 SEO에 유리하며, 서버에서만 실행됩니다.
- 클라이언트 컴포넌트는 사용자 인터랙션, 상태 관리, 이벤트 핸들러에 필요하지만, 번들 크기가 커질 수 있으니 꼭 필요한 곳에만 사용하세요.
-
_app.js, _document.js 같은 특수 파일은 pages 디렉토리에서만 사용 가능하다?
Next.js의 앱 라우팅 시스템과 기존 pages 시스템은 약간 다르기 때문에 혼동될 수 있습니다. 만약 최신 앱 디렉토리를 사용하고 있다면, 그에 맞게 구조를 잡아야 해요.
다음에도 Next.js 라우팅이나 페이지 관련 정보를 더 쉽게 풀어서 알려드릴게요! 필요하면 댓글로 궁금한 점 주세요! 😊
루트 세그먼트부터 해당 페이지까지 전달되는 동적 라우트 파라미터들을 담고 있는 객체를 반환하는 Promise에 대해 살펴볼게요.
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
}
여기서 params
는 Promise 형태로 넘어오기 때문에, 값을 얻어내려면 꼭 async/await
을 써야 합니다. 혹은 React의 use 함수와 같은 방법을 써도 좋아요.
아래는 동적 라우팅에 따른 params
값 예시입니다.
Example Route | 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'] }> |
참고로, Next.js 14버전까지는 params
가 동기 프로퍼티였어요. 그래서 바로 접근 가능했죠. 근데 Next.js 15부터는 이게 Promise 형태가 됐습니다. 아직 15버전에서는 예전처럼 동기적으로 접근해도 동작하지만, 앞으로는 deprecated(사용 중단 예정)이 될 거니까 미리 async/await
패턴으로 바꾸는 게 좋아요.
추가 팁을 드리자면, 동적 라우팅에서 여러 개의 파라미터가 있을 때는 params
객체에 그 이름대로 키가 잡히고, catch-all 라우트([...slug]
)처럼 경로의 여러 값을 배열로 받을 때도 있으니 구조 분해할 때 조심하세요.
요약하자면,
- Next.js 15부터
params
는 Promise다. async/await
나 React의 use 함수로 값을 받아야 한다.- 기존 동기 접근은 당분간 유지되지만, 곧 없어질 예정이다.
이 점만 기억하면 동적 라우팅을 다룰 때 좀 더 깔끔하고 미래에도 문제없는 코드를 작성할 수 있을 거예요!
searchParams (선택 사항)
searchParams
는 현재 URL의 쿼리 파라미터를 담고 있는 객체로, Promise 형태로 전달됩니다. 예를 들어, 이렇게 받아올 수 있죠:
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters;
}
예를 들어 URL이 /shop?a=1
이라면 searchParams
는 { a: '1' }
라는 객체를 Promise로 감싸 전달합니다.
예시 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'] }> |
여기서 중요한 점은, 같은 키에 여러 값이 있을 때는 배열 형태로 받아온다는 거예요. (예: ?a=1&a=2
)
추가 팁을 하나 드리자면, 이 searchParams
를 활용해 필터링 기능이나 페이징 같은 걸 페이지에서 직접 다룰 수 있어서, 서버에서 별도 처리를 하지 않고도 쿼리 기반 UI를 손쉽게 만들 수 있습니다. Next.js 13 이상 같은 최신 프레임워크에서 특히 유용한 패턴이니, 꼭 기억해두세요!
이 내용은 Next.js의 searchParams
prop 사용법에 관한 업데이트를 다루고 있는데요, 쉽게 말해서 searchParams
가 이제는 Promise 형태라는 점을 기억해 두셔야 합니다. 예전(버전 14 이하)에는 동기적으로 바로 접근할 수 있었지만, 버전 15부터는 비동기적으로 값을 받아야 하므로 async/await
이나 React의 비동기 훅을 꼭 써야 한다는 뜻이죠.
주요 포인트 정리
내용 | 설명 |
---|---|
searchParams 가 Promise 형태 | searchParams 가 비동기 값이 되었어요. 따라서 값을 사용할 때 꼭 await 를 사용하거나 비동기 훅을 사용하세요. |
이전 버전과의 호환성 | Next.js 15에서는 이전처럼 동기적으로 접근 가능하지만, 앞으로는 지원이 끊깁니다. 지금부터는 비동기로 처리하는게 안전해요. |
동적 API | searchParams 는 동적인 API라서 해당 값을 미리 알 수 없고, 이를 쓰면 서버가 요청 시점에 페이지를 렌더링 합니다. 따라서 빌드 타임에 완전히 고정된 페이지는 아니게 돼요. |
객체 형태 | searchParams 는 URLSearchParams 인스턴스가 아니라 일반 자바스크립트 객체입니다. 즉, 메서드 같은 건 없고 단순 키-값 쌍이에요. |
좀 더 쉽게 이해할 수 있는 예시
만약 URL에 ?category=books
라는 쿼리파라미터가 붙었다고 해볼게요. 기존에는 이렇게 바로 썼겠죠?
function Page({ searchParams }) {
console.log(searchParams.category);
return <div>{searchParams.category}</div>;
}
하지만 이제는 이렇게 바꿔야 합니다.
async function Page({ searchParams }) {
const params = await searchParams;
console.log(params.category);
return <div>{params.category}</div>;
}
또는 React의 useEffect
를 쓰는 컴포넌트 안이라면:
function PageWrapper() {
const [category, setCategory] = React.useState(null);
React.useEffect(() => {
async function getParams() {
const params = await searchParams;
setCategory(params.category);
}
getParams();
}, []);
return <div>{category ? category : '로딩중...'}</div>;
}
정리하며
- Next.js 15부터는
searchParams
가 비동기로 바뀌었으니,await
안 쓰면 제대로 된 값 못 받아요. - 이번 업데이트로 인해 약간의 코드 수정이 필요하지만, 동적 쿼리에 더 유연하게 대응할 수 있어져서 좋은 점도 많아요.
- 참고로,
searchParams
는 그냥 키-값 객체라서get()
이런 메서드는 없습니다.
앞으로 쿼리파라미터를 다룰 때 이 점 꼭 기억하시고 미리 준비해두세요! 개발할 때 예상치 못한 비동기 문제에 걸려서 당황하는 일이 크게 줄어들 거예요. 혹시 동적 라우팅이나 쿼리 데이터 처리에 대해 더 궁금한 점 있으면 언제든지 알려주세요!
Next.js의 새로운 app 폴더 구조에서는 페이지 컴포넌트에 params
와 searchParams
를 props로 전달할 수 있어요. 이걸 활용하면 동적 경로나 쿼리스트링을 쉽게 처리할 수 있답니다.
먼저, params
를 이용한 예제부터 볼게요. 아래 코드에서 params
는 URL의 동적 세그먼트(예: /blog/[slug]
의 slug)를 의미해요.
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>Blog Post: {slug}</h1>
}
여기서 주목할 부분은 params
가 Promise 형태라는 것! 그래서 await
으로 먼저 값을 꺼내줘야해요. 이렇게 하면 URL에 따라 각기 다른 블로그 포스트를 보여줄 수 있죠.
다음은 searchParams
를 이용해서 URL 쿼리스트링을 가져오는 방법이에요. 예를 들어, ?page=2&sort=desc&query=apple
같은 쿼리가 있을 때 유용하죠.
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>
)
}
searchParams
역시 Promise 형태라서 await
해준 뒤 각 파라미터를 디폴트값과 함께 받는 게 좋아요. 이렇게 하면 필터링, 페이지네이션, 정렬 같은 작업을 쉽게 처리할 수 있어요.
추가 팁
-
params
는 동적 라우팅에서만 전달되고,searchParams
는 URL의 쿼리스트링이 있을 때만 의미가 있습니다. -
쿼리파라미터가 배열 형태일 수도 있으니, 타입을
{ [key: string]: string | string[] | undefined }
같이 지정해주는 게 좋아요. -
await
을 꼭 사용해야 하는데, async 함수인 점을 잊지 마세요! -
실제로는 데이터를 서버에서 가져오는 코드와 결합해서 화면에 뿌리는 경우가 많아요. 이 부분만 분리해서 써도 Next.js의 라우팅과 데이터 패칭이 간단해집니다.
이제 Next.js의 새 라우팅 방식으로 더 편리한 동적 페이지와 쿼리 처리 구현해보세요!
클라이언트 컴포넌트에서 searchParams와 params 읽기
Next.js 13의 앱 라우팅에서는 클라이언트 컴포넌트(Client Component)에서는 async
함수를 사용할 수 없기 때문에, searchParams
와 params
처럼 비동기 데이터를 직접 받아서 처리하는 데 약간의 꼼수가 필요합니다.
바로 React 18에서 도입된 use
훅을 활용하면, 비동기 Promise
형태로 전달되는 params
와 searchParams
를 간단히 읽을 수 있어요.
'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)
// 이 시점에서는 slug와 query가 실제 데이터로 변환되어 사용 가능
}
여기서 중요한 점은 params
와 searchParams
가 Promise
로 전달된다는 것입니다. 그래서 이걸 바로 읽기 위해 use(params)
, use(searchParams)
를 사용하는 거죠. 이렇게 하면 동기적으로 사용할 수 있어서, 클라이언트 컴포넌트에서도 자연스럽게 데이터를 사용할 수 있습니다.
참고로,
use
훅은 아직 실험적인 기능이라서 프로젝트 환경이나 React 버전에 따라 다소 차이가 있을 수 있으니, 최신 공식 문서나 Next.js 업데이트를 꼭 확인하는 게 좋아요.
버전 히스토리
버전 | 내용 |
---|---|
13.4 | 클라이언트 컴포넌트에서 use 훅을 활용해 params 와 searchParams 를 쉽게 읽을 수 있는 기능 추가 |
- | use 훅은 React 18의 실험적 기능으로서, 점차 안정화 중 |
개발하면서 이런 작은 변화들이 실제 코드 작성 방식을 많이 바꾸는데요, 특히 Next.js처럼 SSR과 클라이언트 렌더링이 섞여 있는 환경에서는 비동기 데이터 처리 방법이 중요해집니다. 앞으로도 새로운 React 기능이나 Next.js 업데이트 소식 공유할게요! 도움이 되셨다면 댓글로 남겨주세요 :)
Version | Changes |
---|---|
v15.0.0-RC | params 와 searchParams 가 이제 Promise로 바뀌었어요. 관련해서 codemod 도 준비되어 있으니 참고하세요! |
v13.0.0 | page 가 도입되었습니다. |
요즘 Next.js 업데이트 소식 전해드릴게요! 특히 v15.0.0-RC
부터는 params
와 searchParams
가 비동기 처리 방식으로 바뀌어서, 기존 코드에선 약간의 수정이 필요해요. 다행히 공식에서 자동으로 코드를 바꿔주는 codemod 도구를 제공하고 있어서, 크게 어렵지 않게 적용할 수 있답니다.
또, 좀 더 기본적인 부분으로 돌아가 보면 v13.0.0
에서는 이제 page
가 도입되었는데요, Next.js 사용하시면서 페이지 단위로 구조를 잡을 때 참고하시면 좋아요. 버전 바뀔 때마다 새로운 기능이 추가되니, 자주 공식 문서 한번씩 훑어보는 걸 추천드려요!