계기

설 연휴 기간 동안 사이드 프로젝트를 하기로 했습니다. 프로젝트를 하면서 느낀 것들을 글로 몇 편 써놓긴 했지만, 노션 사이트를 배포해놓았다 하더라도 블로그처럼 열린 공간의 느낌이 아닌 정적 웹 문서에 더 가까웠기 때문에 좀 아쉬운 감이 있었던 차였습니다. 그렇다고 기존 블로그 플랫폼들이 성에 차지는 않아서, 어떻게 할까 고민을 하고 있던 차였는데… 노션을 CMS로 그대로 사용하면서 블로그 뷰만 제공할 수 있으면 좋겠다는 생각이 들었고, 노션 API를 찾아보니 사용하기 쉽게 잘 만들어져 있어서 블로그를 빠르게 만들 수 있겠다는 생각이 들었습니다.

기술적 목표

Notion API의 특성을 이용해 컴포넌트를 열심히 구현해 보는 것이 목표입니다. 정확히는 잘 설계된 API 응답 구조와 컴포넌트의 관계를 살펴 보려고 합니다. 딱히 기술적 목표가 있다기보다는 노션에 글만 쌓아두고 있으면 안 되겠다고 생각 하기도 했고, 이걸 한 번 잘 만들어 두면 나중에 라이브러리로 만들어볼 수도 있지 않을까 하는 생각을 했습니다.

Notion API 분석하기

거두절미하고, 노션에서 쓰는 API 기본 단위는 Block 입니다. 텍스트 문단 하나, 콜아웃, 임베드, 리스트 등 페이지 안에 있는 데이터베이스를 제외한 모든 요소들 은 모두 Block 오브젝트입니다. 심지어 페이지 그 자체도요. (그래서 CRUD API 중 Delete에 해당하는 API는 Block 에만 있습니다)

API Call로 받아볼 수 있는 Block object의 대략적인 생김새는 아래와 같습니다.

{
	"object": "block",
	"id": "c02fc1d3-db8b-45c5-a222-27595b15aea7",

	...,
	//블록의 메타 데이터

	"has_children": true,
	"type": "heading_2",
	"heading_2": {
		...,
		//블록의 실질적인 데이터
		"children": [] //block[]
	}
}

저는 개발 공부를 시작하기 전에도 마크다운 편집 툴을 익숙하게 쓰고 있었지만 마크업 언어라는 개념에 대해서는 모르고 있었는데요, 노션이 마크다운을 지원하지만 마크다운보다 더 넓은 범위의 커스텀을 제공할 수 있는 것은 단순히 마크다운을 html로 파싱하는 것 이상의 설계가 있기 때문이라는 것을 이 API를 보면서 깨달았습니다. 아마 마크다운의 장점을 가져가면서도 표현의 한계 (다단 편집, 임베드 등 이것저것)를 해결하기 위해 이런 식의 설계를 한 것이 아닐까 예상했는데요, 여기저기 구글링을 하다가 노션 기술 블로그에 있는 API 설계 철학에 대해 쓴 문서를 발견했습니다.

<aside> 💡 However, the biggest problem with Markdown is that it is simply not expressive enough to support the use-cases that our users wanted an API to fulfill, such as custom importers and exporters to bring data into and out of Notion, or integrations using Notion as a CMS or backing datastore. People have likened Notion to a "blank canvas" and "a place to do messy thinking," because it’s so flexible and expressive. If our API could not replicate what users have spent valuable time creating in Notion, its power and usefulness would be impaired.

</aside>

노션을 선호하는 유저들이 노션을 빈 캔버스처럼 이것저것 copy and paste하면서 이것저것 생각하고 표현해 보는 공간으로 사용하는 경향을 읽었는데요, API가 마크다운만 지원하면 그런 식의 니즈를 채우지 못할 것이라고 생각했다고 합니다. 실제로 노션은 표 형태의 자료를 웹에서 복사해서 붙여넣기하면 그대로 붙여 넣어지고, 양식이 있었다면 붙여넣기할 때 해당 양식을 대체로 유지합니다. 파일이나 임베드와 같은 형태도 지원하기 때문에 마크다운보다는 훨씬 확장성을 가지면서도 마크다운을 지원합니다. 더 자세한 내용은 여기(페이지 언어 설정을 영어로 바꾸어야 페이지에 접근이 가능합니다)에서 읽을 수 있습니다. 실제로 contentful이나 prismic 같은 headless cms 솔루션들이 있지만, markdown 편집을 지원하면서도 동시에 더 확장성 있는 편집을 할 수 있는 툴은 제가 아는 선에서는 본 적이 없는 것 같습니다.

API 요청 받아보기

그러면 API 요청을 하고 응답을 받아와 보겠습니다. 저는 Next.js 13의 App Router를 사용했고, Incremental Static Regeneration을 이용해 페이지들을 정적 빌드하고 일정 주기마다 재생성할 예정입니다. notion api 엔드포인트로 요청을 하는 작업은 평균 초당 3회 정도로 제한이 걸려 있다고 하는데요, 딱 세 번 까지라는 식으로 엄격하진 않았습니다. 이 횟수를 초과하면 429 코드와 함께 시간이 요청 제한 시간이 헤더에 온다고 합니다. 포스트 여러 개에 대한 API를 빌드 시점에 많이 호출하는 것까지는 제한이 걸리지 않았고, dev 환경에서 호출 횟수를 확인해 보니 server side나 client side에서 여러 번 요청이 들어가게 되면 금방 요청 횟수를 넘겠다는 생각을 해 ISR을 택했습니다. 블로그 특성상 한 번 글이 작성되면 글 수정이 많지도 않을 뿐더러 방문자들이 빠르게 내용을 받아볼 수 있게 되기도 하고요.

Block 객체들에 대한 API 제한 사항들이 있었는데, 빡빡하지는 않은 것 같으나 중간에 글이 잘리거나 하는 일이 벌어질 여지는 충분하다고 생각했습니다. 제일 걸릴만한 제한은 한 문단에 plain text 2000자 제한인 것 같은데, 정상적인 글이라면 한 문단에 2000자까지 쓰지는 않겠지만 이런 제한이 있는 요청은 400 코드와 함께 응답이 온다고 하니까 알아두어야겠습니다. 그 외에도 특정 블록에는 특정 요청은 보낼 수 없다거나 하는 제한들도 있으니 혹시 노션 API로 무언가를 하실 분들은 꼭 이 페이지를 읽어보시길 바랍니다.

API 요청을 위해 @notionhq/client 라는 SDK가 잘 만들어져 있는 것 같아 우선 사용해 보았는데, API 응답 구조상 타입스크립트 에러를 피할 수 없었습니다. 제가 요청을 보내는 API 엔드포인트가 응답 타입에 명시적으로 지정되지 않은 필드를 응답으로 보내거나, 실질적으로 응답으로 가져온 데이터 타입 정의가 광범위하게 되어있어 assertion이나 narrowing하는 작업을 너무 자주 해야 했습니다. 제가 응답을 받고자 하는 형식은 특정 interface로 정의할 수 있었고, 그 이외의 응답은 받지 않을 것이었기 때문에 실제로 응답을 받아본 뒤, 블로그 데이터베이스 컬럼에 대응하는 인터페이스를 직접 작성했습니다. (그리고 이 행동은 나중에 정말 큰 업보로 돌아오게 됩니다)

저는 정적 페이지를 빌드하기로 했으니 다음과 같은 흐름으로 API 요청을 보냅니다:

  1. 블로그 글 데이터베이스에서 ‘published’ 항목에 체크한 글만 시간순으로 쿼리합니다.
  2. 해당 데이터베이스에 적힌 page id들을 가지고 각 페이지의 블록들을 가져오는데요, 한 페이지의 모든 컨텐츠를 가져오려면 블록 단위의 엔드포인트에서 일정 단위(최대 100개)로 페이지네이션하여 가져옵니다.
  3. 노션은 블록들을 BFS 방식으로 제공합니다. (개인적인 생각인데, API 콜 수가 늘어나더라도 한 번에 적은 양의 데이터를 제공할 수 있어 서버 블로킹이 줄어들기 때문에 이렇게 제공하는 것 같습니다) 따라서 중첩 리스트처럼 들여쓰기한 곳이 있다면 재귀적으로 부모 블록에서 해당 블록의 자식 블록 데이터를 따로 얻어와야 합니다.
  4. 각 페이지에서 페이지 메타데이터를 가져오기 위해 페이지 id를 가지고 페이지의 정보를 가져옵니다.