Skip to Content
가이드Next.js App Router

Next.js App Router 가이드

Next.js App Router에서 notion-to-jsx를 통합하는 전체 가이드입니다.

프로젝트 구조

    • notion.ts
    • layout.tsx
      • page.tsx
        • page.tsx
        • PostRenderer.tsx

Notion 클라이언트 (서버 전용)

lib/notion.ts
import { Client } from 'notion-to-utils'; export const notionClient = new Client({ auth: process.env.NOTION_TOKEN, }); export const databaseId = process.env.NOTION_DATABASE_ID;

루트 레이아웃 (CSS 포함)

app/layout.tsx
import 'notion-to-jsx/dist/index.css'; import 'prismjs/themes/prism-tomorrow.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body>{children}</body> </html> ); }

글 목록 페이지 (서버 컴포넌트)

app/posts/page.tsx
import Link from 'next/link'; import { notionClient, databaseId } from '@/lib/notion'; import { extractValuesFromProperties } from 'notion-to-utils'; export default async function PostsPage() { const response = await notionClient.dataSources.query({ data_source_id: databaseId!, }); return ( <ul> {response.results.map((page: any) => { const values = extractValuesFromProperties(page.properties); return ( <li key={page.id}> <Link href={`/posts/${values.Slug}`}> {values.Name} </Link> </li> ); })} </ul> ); }

글 상세 페이지 (서버 컴포넌트)

app/posts/[slug]/page.tsx
import { notionClient, databaseId } from '@/lib/notion'; import PostRenderer from './PostRenderer'; export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; // Slug로 페이지 ID 조회 const { results } = await notionClient.dataSources.query({ data_source_id: databaseId!, filter: { property: 'Slug', rich_text: { equals: slug }, }, }); const pageId = results[0].id; const [blocks, properties] = await Promise.all([ notionClient.getPageBlocks(pageId), notionClient.getPageProperties(pageId), ]); return ( <PostRenderer blocks={blocks} title={(properties?.['Name'] as string) || ''} cover={(properties?.['coverUrl'] as string) || ''} /> ); }

Renderer 래퍼 (클라이언트 컴포넌트)

app/posts/[slug]/PostRenderer.tsx
'use client'; import { Renderer } from 'notion-to-jsx'; import type { NotionBlock } from 'notion-to-jsx'; interface Props { blocks: NotionBlock[]; title?: string; cover?: string; } export default function PostRenderer({ blocks, title, cover }: Props) { return ( <Renderer blocks={blocks} title={title} cover={cover} showToc tocStyle={{ top: '20%' }} /> ); }

서버 + 클라이언트 분리 이유

  • 서버 컴포넌트: 서버 전용 환경변수로 Notion API 데이터 페칭
  • 클라이언트 컴포넌트: Renderer는 클라이언트 사이드 기능 필요 (스크롤, 인터랙티브 목차)

[!TIP] 이 패턴으로 API 토큰은 서버에 유지하면서 UI는 인터랙티브하게 동작합니다.

Last updated on