문제 상황
정적 블로그를 개발하면서 content-collections 라이브러리를 사용하고 있다. 이 라이브러리는 Markdown 파일을 읽어 타입 세이프한 콘텐츠 컬렉션을 만들고, allPosts() 같은 헬퍼 함수를 자동으로 생성해준다.
allPosts()는 블로그 글 목록을 렌더링할 때 자주 사용된다. 하지만 이 함수는 각 글의 본문까지 포함한 전체 데이터를 반환한다. 글 목록에는 본문이 필요 없기 때문에, 이 동작이 의도치 않은 문제를 일으킬 수 있다.
App Router 환경에서 글 목록 페이지는 서버 컴포넌트로 렌더링된다. 이 경우에는 allPosts()를 호출해도 클라이언트 번들에는 영향을 주지 않는다.
하지만 개발자가 실수로 이 데이터를 클라이언트 컴포넌트에 넘기거나, 클라이언트 컴포넌트 내부에서 사용하게 되면, 본문까지 포함된 데이터가 클라이언트로 전달되고 번들 사이즈가 불필요하게 커진다.
이 글에서는 그런 실수를 방지하기 위한 구조적 해결 방법을 정리한다.
원인 분석
allPosts()는 아래와 같은 구조의 객체 배열을 반환한다:
{
slug: 'example-post',
data: {
title: 'Example Post',
date: '2024-01-01',
thumbnail: '/thumb.jpg',
tags: ['nextjs', 'content'],
},
body: '<h1>Example</h1> ...' // 본문 전체
}글 목록에는 title, date, thumbnail 정도만 필요하다. 하지만 실수로 이 전체 데이터를 클라이언트에 넘기면 다음과 같은 문제가 발생한다:
- 클라이언트로 전달되는 데이터 크기가 커진다
- 초기 페이지 로딩 시 큰 JSON 페이로드가 전송된다
- 글 수가 많아질수록 번들 사이즈가 계속 증가한다
해결 방법
본문을 포함하지 않고, 글 목록에 필요한 필드만을 담는 전용 컬렉션을 defineCollection()을 사용해 별도로 정의한다.
import { defineCollection } from "content-collections";
export const postMetas = defineCollection({
name: "postMetas",
pattern: "posts/*/index.md",
schema: ({ z }) =>
z.object({
title: z.string(),
date: z.string(),
thumbnail: z.string(),
tags: z.array(z.string()),
}),
});이 컬렉션은 Markdown 본문을 포함하지 않으며, 필요한 메타 정보만을 안전하게 제공한다.
글 목록에서는 자동 생성된 allPostMetas() 함수를 사용해 메타데이터만 불러온다:
// app/page.tsx
import { allPostMetas } from "content-collections";
import PostList from "@/components/PostList";
export default function HomePage() {
const posts = allPostMetas();
return <PostList posts={posts} />;
}현재 PostList는 서버 컴포넌트이기 때문에 문제가 없지만, 미래에 클라이언트 컴포넌트로 변경되더라도 안전하게 사용할 수 있도록 구조를 미리 분리한 것이다.
마무리
글 목록 구성에 본문이 포함된 전체 데이터를 사용하는 것은 눈에 띄지 않지만, 점점 커지는 번들 사이즈로 이어질 수 있다. 초기에는 서버 컴포넌트에서 문제가 없어 보여도, 클라이언트로 데이터를 넘기거나 컴포넌트 구조가 바뀌는 순간 성능 저하의 원인이 될 수 있다.
메타데이터 전용 컬렉션을 분리해 사용하는 것은 이런 실수를 구조적으로 방지하는 방법이다. 지금은 필요 없어 보여도, 미래의 나를 위한 좋은 예방 조치가 된다.
