1. 요약
- 아직까지는 간단한 수준에 머물러있습니다 🙂
- 예시 포스트 스크린샷:
2. 목적
- 노션으로 작성한 글들을 커스텀 UI/UX로 게시하기 위해 HTML로 빌드하고자 직접 구현합니다.
2-1. 커스텀 렌더링이 필요한 경우, 아닌 경우
- 블로그를 덜 노션답게 만들려고 한다면, 노션을 CMS로 활용할 거라면 필요합니다.
- 노션 컨텐츠를 노션의 형식처럼 보여주기만 하면 된다면 Notion Sites로 매우 충분합니다.
NotionBuild a website with Notion in seconds, no coding required

Build a website with Notion in seconds, no coding required
Company websites, team blogs, personal resumes and portfolios — you can build them all with Notion Sites. Skip the cumbersome web developers and create a website without any coding.
- 타 블로그 예시(oopy를 사용했지만, Notion Sites로도 가능합니다):
개발하려면 평생 배워야지 | 일신우일신개발하려면 평생 배워야지 | 일신우일신개발하려면 평생 배워야지 | 일신우일신
성장에 목마른 개발자가 남기는 흔적입니다. 배우고, 느끼고, 경험한 모든 것의 흔적을 남기고 있습니다. 현재 서울에서 SSAFY 9기를 이수하며 Full Stack 커리어를 밟고 있습니다. 파이썬으로 코딩하는 것을 즐깁니다.
2-2. 기존 라이브러리를 사용할 경우, 아닌 경우
- 기존 라이브러리가 존재합니다:
- React 기반으로 노션과 유사하게 렌더링해줍니다.
- 단, 공식 Open API를 사용하지 않아 Public Page만 렌더링이 가능합니다.
- 공식 Open API를 입력으로 렌더링하는 라이브러리도 있습니다.
GitHubreact-notion-x/packages/notion-compat at master · NotionX/react-notion-xreact-notion-x/packages/notion-compat at master · NotionX/react-notion-x
Fast and accurate React renderer for Notion. TS batteries included. ⚡️ - NotionX/react-notion-x
- 이미 나와있는 라이브러리를 사용하지 않고, Notion API를 실제로 다뤄보면서 어떤 문제점이 있는지를 알고 진행하기 위해 직접 구현합니다.
3. 방법
2-1. 요약
- Notion API 혹은 SDK를 사용해 JSON을 가져옵니다.
- JSON을 HTML로 변환합니다.
2-2. 페이지 구성 요소 fetch 해오기 (Notion API/SDK 사용)
- 2가지 방법이 있습니다.
- Notion API:
Notion APINotion API Overview

Notion API Overview
Discover how to leverage Notion's Public API to build integrations.
- Notion SDK: notion-sdk-jsGithubnotion-sdk-jsOwnermakenotionUpdatedJun 18, 2025
- SDK가 Request, Response Type을 제공하기 때문에 사용하는 게 좋습니다.
- TypeScript로 Notion SDK 제어 부분을 짠다면 결국 해당 SDK를 만들게 될 듯해서, 저는 사용했습니다 🙏
2-2-1. 데이터 구조 파악
- 렌더링 시에는 일부 프로퍼티들(
id, properties)만 사용합니다.
- 페이지 예시 JSON
{ "object": "page", "id": "18453cfa-96ad-80dd-8e50-d465ac1af643", "created_time": "2025-01-23T06:32:00.000Z", "last_edited_time": "2025-02-11T12:09:00.000Z", "created_by": { "object": "user", "id": "0495b827-bd4e-4f3d-8549-02cde40e987f" }, "last_edited_by": { "object": "user", "id": "0495b827-bd4e-4f3d-8549-02cde40e987f" }, "cover": { "type": "external", "external": { "url": "https://www.notion.so/images/page-cover/webb4.jpg" } }, "icon": null, "parent": { "type": "database_id", "database_id": "6b7db292-c7d5-44cb-a34d-965012a736a4" }, "archived": false, "in_trash": false, "properties": {}, "url": "https://www.notion.so/...", "public_url": "https://seongbin9786.notion.site/...", "request_id": "502eb255-e42a-4989-b07b-376bff8a5c0b" }
2-2-2. 데이터 가져오기
- 페이지와 관련된 데이터는 크게 2가지입니다.
- 페이지 메타데이터
Notion APIPage

Page
The Page object contains the page property values of a single Notion page. { "object": "page", "id": "be633bf1-dfa0-436d-b259-571129a590e5", "created_time": "2022-10-24T22:54:00.000Z", "last_edited_time": "2023-03-08T18:25:00.000Z", "created_by": { "object": "user", "id": "c2f20311-9e54-4d11-8c79-73...
Notion APIRetrieve a page

Retrieve a page
🚧 This endpoint will not accurately return properties that exceed 25 references: Do not use this endpoint if a page property includes more than 25 references to receive the full list of references. Instead, use the Retrieve a page property endpoint for the specific property to get its complete refe...
- 페이지 컨텐츠 (Block)
Notion APIWorking with page content

Working with page content
Learn about page content and how to add or retrieve it with the Notion API.
Notion APIRetrieve block children

Retrieve block children
Returns a paginated array of child block objects contained in the block using the ID specified. In order to receive a complete representation of a block, you may need to recursively retrieve the block children of child blocks. 👍 Page content is represented by block children. See the Working with pa...
- 페이지 메타데이터는
notion.pages.retrieve함수를 사용합니다.
const { Client } = require('@notionhq/client'); const notion = new Client({ auth: process.env.NOTION_API_KEY }); (async () => { const pageId = '59833787-2cf9-4fdf-8782-e53db20768a5'; const response = await notion.pages.retrieve({ page_id: pageId }); console.log(response); })();
- Notion JS SDK의
notion.blocks.children.list함수를 사용합니다. - 시작점이 될 blockId는 pageId를 사용합니다.
const { results } = await notion.blocks.children.list({ block_id: blockId, start_cursor: cursor, });
2-2-3. 재귀적(DFS)으로 데이터 가져오기
- 각 블록의 children을 조회하면 개별 블록 조회를 할 필요가 없습니다.
- 완전 탐색으로 조회합니다.
- 시작 파라미터(root blockId): pageId
- 종료 조건:
response.has_more - 조회 파라미터:
response.next_cursor
- 임의로 페이지 블록 Object의
children속성에 children 블록 배열을 할당합니다.
- 예시 코드
export async function fetchBlocksRecursively( blockId: string, ): Promise<BlockObjectResponseWithChildren[]> { const allBlocks: BlockObjectResponseWithChildren[] = []; let cursor: string | undefined = undefined; while (true) { const response: ListBlockChildrenResponse = await notion.blocks.children.list({ block_id: blockId, start_cursor: cursor, }); const blockResults = response.results as BlockObjectResponseWithChildren[]; // 각 블록을 순회하면서, has_children = true이면 또 재귀적으로 children을 fetch for (const block of blockResults) { if (block.has_children) { // 자식 블록들을 가져온 뒤, block.children 필드에 저장 const childBlocks = await fetchBlocksRecursively(block.id); block.children = childBlocks; } } allBlocks.push(...blockResults); if (!response.has_more) { break; } cursor = response.next_cursor || undefined; } return allBlocks; }
2-3. JSON → HTML로 변환하기
- HTML으로 변환한다는 것은 기본적으로 string으로 HTML 형태를 만들고, 파일로 출력한다는 것을 의미합니다.
2-3-1. HTML 틀 만들기
- 인라인 스타일 대신 별도의 CSS 파일을 사용합니다. (e.g.
basic.css)
- 코드 하이라이팅은 HTML,CSS로는 어려워 외부 라이브러리(e.g. prism)를 사용합니다.
const html = ` <!doctype html> <html> <head> <!-- prism은 코드 하이라이터 --> <link rel="stylesheet" href="../prism/prism.css" /> <script src="../prism/prism.js"></script> <link rel="stylesheet" href="../basic.css" /> </head> <body> ${renderBlocks(blockTree)} </body> </html> `;
2-3-2. 블록 재귀 렌더링하기
- 위에서 페이지 조회 시 모든 블록을 재귀적으로 조회하여,
children프로퍼티에 배열로 저장해두었습니다.
- 기본적으로 블록 단위로 string으로 변환하고, 하나로 합치면 됩니다(
join(”")).
- 우선 타입을 확장해야 합니다. SDK의 타입에는
children속성이 없기 때문입니다.
import type { BlockObjectResponse } from "@notionhq/client/build/src/api-endpoints"; export type BlockObjectResponseWithChildren = BlockObjectResponse & { children?: BlockObjectResponseWithChildren[]; };
- 재귀적인 렌더링을 구현하려면, 각 블록 렌더링 시 children을 감싸서 본인의 HTML에 포함시키고, children은 하나의 문자열을 반환하면 됩니다.
function renderBlock(block: BlockObjectResponseWithChildren): string { const { type, has_children, children } = block; const childrenHTML = has_children && children && children.length > 0 ? renderBlocks(children) : ""; switch (type) { case "타입": { const { rich_text } = block.타입; return `<p>${renderRichText(rich_text)}${childrenHTML}</p>\n`; } // ... 생략 } }
export function renderBlocks( blocks: BlockObjectResponseWithChildren[], ): string { return blocks.map((block) => renderBlock(block)).join("\n"); }
- renderBlock 함수에서
type에 따라 HTML으로 변환해서 반환하면 됩니다.
- SDK를 사용하는 경우 다음과 같이 type 자동 완성이 됩니다.
- 또한 type 별로 response가 별도 정의되어 있어 type 자동 완성이 됩니다.
2-3-3. 나머지 type 처리하기
- 예시 GitHub 코드를 참고해주세요: renderer.tsGithubrenderer.tsOwnernotion-utils
- type 별 처리 방법은 별도로 정리할 수도 있을 것 같습니다 🙏