1. 요약2. 목적2-1. 커스텀 렌더링이 필요한 경우, 아닌 경우2-2. 기존 라이브러리를 사용할 경우, 아닌 경우3. 방법2-1. 요약2-2. 페이지 구성 요소 fetch 해오기 (Notion API/SDK 사용)2-2-1. 데이터 구조 파악2-2-2. 데이터 가져오기2-2-3. 재귀적(DFS)으로 데이터 가져오기2-3. JSON → HTML로 변환하기2-3-1. HTML 틀 만들기2-3-2. 블록 재귀 렌더링하기2-3-3. 나머지 type 처리하기
1. 요약
- 아직까지는 간단한 수준에 머물러있습니다 🙂
- 예시 포스트 스크린샷:
2. 목적
- 노션으로 작성한 글들을 커스텀 UI/UX로 게시하기 위해 HTML로 빌드하고자 직접 구현합니다.
2-1. 커스텀 렌더링이 필요한 경우, 아닌 경우
- 블로그를 덜 노션답게 만들려고 한다면, 노션을 CMS로 활용할 거라면 필요합니다.
- 노션 컨텐츠를 노션의 형식처럼 보여주기만 하면 된다면 Notion Sites로 매우 충분합니다.
- ‣
- 타 블로그 예시(oopy를 사용했지만, Notion Sites로도 가능합니다):
- ‣
2-2. 기존 라이브러리를 사용할 경우, 아닌 경우
- 기존 라이브러리가 존재합니다:
- React 기반으로 노션과 유사하게 렌더링해줍니다.
- 단, 공식 Open API를 사용하지 않아 Public Page만 렌더링이 가능합니다.
- 공식 Open API를 입력으로 렌더링하는 라이브러리도 있습니다.
- ‣
- 이미 나와있는 라이브러리를 사용하지 않고, Notion API를 실제로 다뤄보면서 어떤 문제점이 있는지를 알고 진행하기 위해 직접 구현합니다.
3. 방법
2-1. 요약
- Notion API 혹은 SDK를 사용해 JSON을 가져옵니다.
- JSON을 HTML로 변환합니다.
2-2. 페이지 구성 요소 fetch 해오기 (Notion API/SDK 사용)
- 2가지 방법이 있습니다.
- Notion API: ‣
- Notion SDK: notion-sdk-jsGithubnotion-sdk-jsOwnermakenotionUpdatedFeb 17, 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가지입니다.
- 페이지 메타데이터
- ‣
- ‣
- 페이지 컨텐츠 (Block)
- ‣
- ‣
- 페이지 메타데이터는
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 별 처리 방법은 별도로 정리할 수도 있을 것 같습니다 🙏