ETC
  • Development

Notion API를 활용한 정적 콘텐츠 관리

2024년 04월 06일

모든 건 새로운 프로젝트 기획에서 시작됐다.
목표는 작성된 아티클 콘텐츠를 보여줄 수 있는 서비스를 만드는 것이었다.

이를 위해 먼저 몇 가지 전제를 정리했다.

풍부한 아티클을 만들기 위해서는 텍스트뿐 아니라 이미지 등 다양한 구성 요소가 필요하다. 그래서 Markdown 문법으로 작성된 콘텐츠를 정적 페이지 형태로 보여주는 방식이 적합하다고 판단했다.

결론적으로 Notion API로 전달받은 콘텐츠를 Markdown 형태로 변환해 화면에 보여주는 구상으로 개발을 진행했다.

Notion API 알아보기

Notion에서는 Workspace 데이터를 REST API로 활용할 수 있는 공개 API를 제공한다. 기본적인 사용 방법은 공식 문서 가이드에 잘 정리돼 있다.

다만 Notion API를 살펴보면서 몇 가지 고려해야 할 점이 있었다.

The rate limit for incoming requests per integration is an average of three requests per second.

당장 기능 구현에 치명적인 문제라고 보진 않아, 이 부분은 추후 개선 포인트로 남기고 진행했다.

자세한 내용은 Request limits를 참고하면 된다.

Notion 공식 문서에서는 API 활용을 돕기 위해 자바스크립트 기반 라이브러리인 @notionhq/client를 함께 소개하고 있다. Auth Token만 설정하면 비교적 쉽게 Notion 데이터를 활용할 수 있다.

const { Client } = require("@notionhq/client");
const notion = new Client({
auth: process.env.NOTION_TOKEN,
});

아래와 같이 콘텐츠를 관리할 데이터베이스를 지정하고, 해당 데이터베이스의 페이지 목록을 가져온 뒤 id를 기반으로 각 페이지의 상세 정보를 조회할 수 있다.

// 데이터베이스 목록 불러오기
const pages = await notion.databases.query({
database_id: databaseId,
});
// 페이지 상세 정보 가져오기
const page = await notion.pages.retrieve({
page_id: pageId,
});

Notion 데이터를 변환하자

API를 통해 데이터를 요청해 보니 예상하지 못한 부분이 있었다. 우리가 일반적으로 알고 있는 Markdown 구조와 Notion의 데이터 구조가 꽤 다르다는 점이었다.

Notion은 정보를 Block 단위의 객체 형태로 구성한다. 덕분에 Markdown 기반 콘텐츠보다 훨씬 다양한 방식으로 표현할 수 있고, 이 특성은 API 응답에도 그대로 반영된다.

이미지

당시 일정이 넉넉하지 않았기 때문에 이 구조를 직접 변환하는 방식은 제외했다. 대신 이미 잘 만들어진 오픈소스를 찾아보던 중 notion-to-md를 발견했다.

이 패키지를 사용하면 Notion API의 Block 객체를 Markdown 형태로 변환할 수 있다.

const { Client } = require("@notionhq/client");
const { NotionToMarkdown } = require("notion-to-md");
const notion = new Client({
auth: "your integration token",
});
const n2m = new NotionToMarkdown({
notionClient: notion,
separateChildPage: false,
});
(async () => {
const mdblocks = await n2m.pageToMarkdown("target_page_id");
const mdString = n2m.toMarkdownString(mdblocks);
console.log(mdString.parent);
})();

변환된 데이터는 다음과 같은 형태다.

{
"parent": "\n# This is parent page ..."
}

특정 Notion Block 커스텀

Markdown 형태로 변환된 데이터를 활용해 아티클을 보여줄 수 있게 됐지만, 한 가지 문제가 남아 있었다. Notion의 모든 Block 요소가 Markdown으로 변환되지는 않는다는 점이다.

대표적인 예가 Callout 블록이다.

이미지

아이콘이나 이미지를 포함한 구조는 Markdown 문법으로 표현하기 어려워, 별도의 커스텀 과정이 필요했다.

notion-to-md는 특정 Block 타입에 대해 커스텀 변환 로직을 제공한다.

const { NotionToMarkdown } = require("notion-to-md");
const n2m = new NotionToMarkdown({ notionClient: notion });
n2m.setCustomTransformer("embed", async (block) => {
const { embed } = block as any;
if (!embed?.url) return "";
return `<figure>
<iframe src="${embed?.url}"></iframe>
<figcaption>${await n2m.blockToMarkdown(embed?.caption)}</figcaption>
</figure>`;
});

이렇게 하면 Markdown 문법으로 구현하기 어려운 요소들도 원하는 형태로 렌더링할 수 있다.

이미지

전체 흐름은 다음과 같이 정리할 수 있다.

이미지

이번 프로젝트에서는 스타일링 라이브러리로 Tailwindcss를 활용했고, Markdown 렌더링은 react-markdown을 사용했다.

<Markdown
components={{
h1(props) {
return <h1 className="text-blue-400" {...props} />;
},
}}
/>

초기 렌더링 속도 개선하기

Notion API 요청 자체가 느린 탓인지, 페이지 초기 렌더링이 꽤 답답하게 느껴졌다. 초기 콘텐츠 렌더링이 느린 것은 UX에 좋지 않다고 판단해 개선을 진행했다.

이미지

기존에는 서버 컴포넌트(RSC) 내부에서 직접 데이터를 요청해 사용했다. 하지만 아티클 콘텐츠의 특성상 변경 빈도가 높지 않다는 점을 고려해 Next.js의 정적 사이트 생성(SSG)을 적용하기로 했다.

정적 생성은 Next.js 13부터 generateStaticParams를 통해 가능하다.

두 가지 방식이 있었는데,

둘 다 초기 렌더링은 빠르지만, 빌드 시간네트워크 의존성에서 차이가 있었다.

API 요청 방식은 빌드 타임에 네트워크 지연이 그대로 반영되고, 네트워크 오류가 발생하면 빌드가 실패할 수 있다. 또한 매 빌드마다 Notion 콘텐츠를 최신화하기 때문에 업데이트 시점을 제어하기 어렵다.

이런 이유로 프로젝트에 Markdown 파일을 저장하는 방식을 선택했다.

이를 위해 Notion API를 활용해 Notion 페이지를 Markdown 형태로 다운로드하는 스크립트를 작성하고, 스크립트 실행 명령어는 package.json에 정의했다.

{
"scripts": {
"download-notion": "rm -rf [아티클 폴더 경로] && mkdir [아티클 폴더 경로] && node fetch-md.mjs"
}
}

이렇게 변경한 뒤 초기 렌더링 시간이 약 3초가량 줄어들었고, 체감상 확실히 빨라졌다.

Notion 이미지 URL 만료 문제

Markdown 파일을 프로젝트에 다운로드하는 방식으로 바꾸면서 새로운 문제가 생겼다. 그중 하나가 이미지 URL 만료 문제였다.

Notion 이미지는 AWS S3에 저장되며, API로 내려오는 이미지 URL에는 시간 제한이 있다. 이 URL을 그대로 Markdown에 저장하면 일정 시간이 지난 뒤 이미지를 불러올 수 없게 된다.

이를 해결하기 위해 웹에 게시된 Notion 페이지의 이미지 URL 형식을 활용했다.

기존 이미지 URL의 필수 파라미터만 추출해 공개 페이지 이미지 경로로 변환하는 함수를 작성했다.

const transferImgSrc = (orgSrc, blockId) => {
// 이미지 URL 변환 로직
};

해당 로직을 "image" 타입 Block 커스텀에 적용해, Markdown 파일에는 항상 유효한 이미지 URL이 저장되도록 했다.

웹에 게시된 페이지가 검색 결과에 노출될 수 있지만, 검색 엔진 인덱싱을 막는 옵션이 있어 이미지 경로 용도로만 활용할 수 있었다.

이미지

Markdown 렌더링 중 스타일 미적용 문제

이 문제는 다운로드 방식 자체보다는 설정 누락에 가까웠다.

Tailwindcss의 content 옵션은 클래스 이름을 감지해 CSS를 생성할 파일 범위를 지정한다. Markdown을 다운로드해서 관리하면서, 커스텀 Block에 포함된 클래스들이 감지되지 않아 스타일이 누락됐다.

이를 해결하기 위해 다운로드 스크립트 파일을 content 옵션에 추가했다.

Markdown 파일 자체를 glob 패턴으로 포함시킬 수도 있었지만, 커스텀 스타일이 정의된 스크립트 파일만 지정하는 방식을 선택했다. 자세한 내용은 Tailwindcss Content Configuration을 참고하면 된다.

module.exports = {
content: ["./fetch-md.mjs"],
};

결론

이번 프로젝트를 통해 Notion API로 어디까지 구현할 수 있는지, 그리고 어떤 한계가 있는지를 비교적 명확히 알 수 있었다.

Notion의 편집 UI는 여전히 강력하다. 물론 이미 익숙한 사람 기준의 평가일 수도 있고, Notion이 여전히 어렵다고 느끼는 사람들도 있다. Markdown 기반이라는 점에서 개발자인 내가 더 편하게 느낀 부분도 분명 있다.

그럼에도 불구하고 표현력과 확장성 측면에서 Notion은 분명한 장점이 있다. 이미 Notion에 익숙한 팀과 함께 일하고 있고, 그 콘텐츠를 프로덕션에 연결해야 한다면 이 방식은 충분히 고려해볼 만하다.

참고