페이지 컴포넌트 작성
이제까지 Next.js App Router의 기본적인 라우팅 원리와 구조에 대해 충분히 익히셨을 겁니다. 이제 그 핵심인 페이지 컴포넌트(Page Component) 를 어떻게 효과적으로 작성하고 활용하는지에 대해 심도 있게 다룰 차례입니다. 페이지 컴포넌트는 사용자가 웹 브라우저를 통해 직접 마주하게 되는 UI의 가장 바깥 영역이자, 특정 URL 경로에 매핑되는 Next.js의 핵심 빌딩 블록입니다.
이 절에서는 페이지 컴포넌트의 기본적인 역할부터 데이터 페칭, 그리고 동적인 파라미터 활용까지, 실제 애플리케이션 개발에 필요한 구체적인 작성 방법을 살펴보겠습니다.
페이지 컴포넌트의 역할과 특징
App Router에서 페이지 컴포넌트는 app
디렉터리 내의 특정 라우트 세그먼트 폴더 안에 위치한 page.tsx
(또는 .js
, .jsx
) 파일입니다.
주요 특징
- URL 매핑:
app/your-route/page.tsx
파일은your-route
경로에 접근했을 때 렌더링되는 UI를 정의합니다. - 최종 UI 렌더링: 레이아웃 컴포넌트와 달리, 페이지 컴포넌트는
children
prop을 받지 않습니다. 대신, 레이아웃의children
prop 위치에 자신만의 고유한 UI를 렌더링합니다. - 기본적으로 서버 컴포넌트: 별도의
"use client"
지시어가 없다면, 페이지 컴포넌트는 서버 컴포넌트로 동작합니다. 이는 페이지 컴포넌트 내에서 직접 데이터베이스에 접근하거나 서버 전용 코드를 작성할 수 있음을 의미합니다. - 비동기 함수 지원: 서버 컴포넌트인 페이지 컴포넌트는
async / await
문법을 사용하여 비동기 데이터 페칭을 직접 수행할 수 있습니다.
기본적인 페이지 컴포넌트 작성하기
가장 기본적인 페이지 컴포넌트는 단순한 React 함수 컴포넌트와 동일하게 작성됩니다.
// src/app/welcome/page.tsx (새로 생성할 페이지)
// 이 컴포넌트는 기본적으로 서버 컴포넌트로 동작합니다.
export default function WelcomePage() {
return (
<div>
<h2>새로운 환영 페이지</h2>
<p>Next.js App Router로 만든 간단한 페이지입니다.</p>
</div>
);
}
실습:
src/app/welcome
폴더를 만들고 그 안에 page.tsx
파일을 위 내용으로 생성한 다음, http://localhost:3000/welcome
으로 접속해 보세요. 페이지가 정상적으로 렌더링되는 것을 확인할 수 있습니다.
페이지 컴포넌트에서 데이터 페칭하기
페이지 컴포넌트가 서버 컴포넌트라는 점은 데이터 페칭에서 엄청난 강점을 발휘합니다. 브라우저(클라이언트)가 아닌 서버에서 데이터를 미리 가져와 HTML을 생성하므로, 사용자는 더 빠른 초기 로딩과 향상된 SEO를 경험할 수 있습니다.
페이지 컴포넌트 내에서 async
함수로 데이터를 가져올 수 있습니다. Next.js는 이 비동기 작업이 완료될 때까지 기다린 후 페이지를 렌더링합니다.
// src/app/posts/page.tsx (새로 생성할 페이지)
interface Post {
id: number;
title: string;
body: string;
}
// 이 함수는 서버에서 실행되어 데이터를 가져옵니다.
async function getPosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
// revalidate 옵션을 사용하여 데이터 캐싱 전략을 지정할 수 있습니다.
// next: { revalidate: 60 } // 60초마다 데이터 갱신
});
if (!res.ok) {
// 에러 발생 시 처리
throw new Error('Failed to fetch posts');
}
return res.json();
}
export default async function PostsPage() { // async 키워드를 붙여 비동기 컴포넌트로 만듭니다.
const posts = await getPosts(); // 서버에서 데이터 페칭
return (
<div>
<h1>모든 게시물</h1>
<ul>
{posts.map((post) => (
<li key={post.id} style={{ marginBottom: '15px', border: '1px solid #eee', padding: '10px' }}>
<h3>{post.title}</h3>
<p>{post.body.substring(0, 100)}...</p>
{/* Link 컴포넌트로 동적 라우트 연결 */}
<Link href={`/posts/${post.id}`}><a>더 보기</a></Link>
</li>
))}
</ul>
</div>
);
}
실습:
src/app/posts
폴더를 만들고 그 안에 page.tsx
파일을 위 내용으로 생성합니다. 그리고 동적 라우트를 위한 src/app/posts/[id]/page.tsx
파일도 다음과 같이 생성합니다.
// src/app/posts/[id]/page.tsx
interface PostDetailPageProps {
params: {
id: string; // URL에서 추출될 게시물 ID
};
}
interface Post {
id: number;
title: string;
body: string;
}
// 특정 게시물 데이터를 가져오는 함수
async function getPost(id: string): Promise<Post> {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!res.ok) {
throw new Error('Failed to fetch post');
}
return res.json();
}
// 동적 라우트를 위한 generateStaticParams (SSG 사용 시)
export async function generateStaticParams() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts: Post[] = await res.json();
// 상위 10개의 게시물만 미리 생성하도록 제한 (실제 프로젝트에서는 전체 또는 필요한 부분만)
return posts.slice(0, 10).map((post) => ({
id: post.id.toString(), // id는 문자열이어야 합니다.
}));
}
export default async function PostDetailPage({ params }: PostDetailPageProps) {
const { id } = params;
const post = await getPost(id); // 서버에서 특정 게시물 데이터 페칭
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
<Link href="/posts"><a>목록으로 돌아가기</a></Link>
</div>
);
}
http://localhost:3000/posts
로 접속하여 게시물 목록을 확인하고, 각 게시물의 "더 보기" 링크를 클릭하여 상세 페이지로 이동해 보세요. 모든 데이터 페칭이 서버에서 이루어져 페이지 로딩이 매우 빠르게 느껴질 것입니다.
동적 파라미터 params
활용하기
동적 라우트([slug]
, [id]
등)를 정의한 경우, 페이지 컴포넌트는 params
라는 prop을 통해 URL에서 추출된 동적인 값을 전달받습니다. 이는 페이지 콘텐츠를 해당 파라미터에 따라 다르게 렌더링할 때 사용됩니다.
위 src/app/posts/[id]/page.tsx
예시에서 params.id
를 사용하여 특정 게시물의 데이터를 가져오는 것을 이미 살펴보았습니다.
// 페이지 컴포넌트의 prop 타입 정의
interface MyPageProps {
params: {
dynamicParamName: string; // [dynamicParamName]
// 만약 Catch-all 세그먼트 [[...slug]]라면:
// slug?: string[];
};
searchParams?: { [key: string]: string | string[] | undefined }; // 쿼리 파라미터 (다음 절에서 다룸)
}
export default async function MyDynamicPage({ params }: MyPageProps) {
const { dynamicParamName } = params;
// ...
}
params
객체의 키(key)는 폴더 이름의 대괄호 안에 정의된 이름([dynamicParamName]
)과 정확히 일치해야 합니다.
페이지 컴포넌트의 추가 기능 (선택 사항)
-
loading.tsx
와 함께 사용: 데이터 페칭 중인 동안 사용자에게 로딩 UI를 보여주고 싶다면, 해당 페이지 컴포넌트와 동일한 라우트 세그먼트 폴더에loading.tsx
파일을 생성하면 됩니다.src/app/posts/loading.tsx // src/app/posts/loading.tsx export default function Loading() { return <div>게시물 목록을 불러오는 중입니다...</div>; }
이제
/posts
로 접속하면 데이터 로딩이 완료되기 전까지 "게시물 목록을 불러오는 중입니다..." 메시지가 잠시 표시될 것입니다. -
error.tsx
와 함께 사용: 페이지 컴포넌트나 그 하위 컴포넌트에서 에러가 발생했을 때 사용자에게 친절한 에러 메시지를 보여주고 싶다면,error.tsx
파일을 생성할 수 있습니다.src/app/posts/error.tsx // src/app/posts/error.tsx "use client"; // Error Boundaries는 클라이언트 컴포넌트여야 합니다. import { useEffect } from 'react'; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { // 에러 로깅 서비스 등에 에러를 기록할 수 있습니다. console.error(error); }, [error]); return ( <div> <h2>문제가 발생했습니다!</h2> <p>{error.message}</p> <button onClick={ // 에러를 재설정하고 다시 시도합니다. () => reset() } > 다시 시도 </button> </div> ); }
error.tsx
파일은 클라이언트 컴포넌트여야 하며, React Error Boundary처럼 동작합니다.
페이지 컴포넌트는 Next.js 애플리케이션의 핵심적인 부분이며, 서버 컴포넌트로서의 강력한 데이터 페칭 능력과 유연한 파라미터 처리는 현대 웹 개발에 필수적인 요소입니다. 이들을 잘 활용하여 사용자에게 빠르고 안정적인 웹 경험을 제공하는 페이지를 구축할 수 있기를 바랍니다.