셈틀 블로그는 어떻게 만들어졌나 (노션)

2025. 12. 26.
카테고리
게시일
Dec 26, 2025
현재 이 블로그는 노션을 일종의 CMS로 사용하고 있습니다. 아래는 이 블로그의 구조를 설명합니다.

왜 노션을 쓰냐?

이거저거 쓸만한 CMS가 많은데, 왜 하필 노션을 쓰냐?
  1. 노션에는 기본 MD 문법 말고도 여러가지 좋은 블럭들을 지원한다.
  1. 이미 평소에 많은 문서들을 노션으로 작성중이라, 문서를 포스팅하고 싶을 때 복붙만 하면 바로 포스팅이 가능하다.

노션을 쓰면 안 좋은점

원래 노션은 전문적인 CMS로 쓰라고 만든 서비스는 아닙니다. 그래서 몇 가지 부족한 부분이 있습니다.
  • 공식에서 렌더러를 지원 안함
    • react-notion-x라는 서드파티 프레임워크를 사용해야하는데, 이게 서드파티다보니 지원이 완벽하지 않은 점이 있고. 커스텀도 좀 불편함.
  • 느림
    • 데이터를 불러오는데 지연시간 큼
  • 쿼리 지원 안하는 게 많음
    • 제대로 된 SQL같은게 있는게 아니고 Notion의 자체개발한 API를 사용하는데, 이게 뭔가 형용할 수 없는 불편한게 많음. 대표적으로 검색 쿼리 날리는게 조금 불편. (이 부분은 Notion 측에서도 꾸준히 업데이트를 하면서 개선중이다.)

인프라

  • Headless CMS : Notion (블로그 카테고리와 같은 메타데이터 + 블로그 포스트 편집 및 데이터 제공)
  • DB : Postgres (Supabase)
노션에서 바로 조회한 뒤 FE로 보내면 노션 API의 절망적인 지연시간으로 무척 느려진다. 포스트 리스트와 카테고리 데이터 같은건 별도 DB에 캐싱해놓았다.
  • Notion - DB 동기화 : Jenkins (스케줄러) + node
  • FE : Next.js
Notion-DB 동기화 스크립트 + jenkinsfile과 Next.js는 Monorepo로 한 군데 모아서 관리.

세부사항

Notion - DB 동기화

실행환경

Notion에서부터 데이터를 받아와 DB에 캐싱하는 node 스크립트를 만들었다. Jenkins에 스케줄러로 주기적으로 스크립트가 실행된다. Jenkins 서버는 자체 호스팅하고 있다.

Notion Database 에서 포스트 정보 추출

우선 Notion Database에서 포스트의 정보를 추출해야 한다. @notionhq/client 를 사용했다. Notion에서 제공하는 공식 라이브러리이다.

추출된 정보를 Supabase에 적용

Supabase-js 를 이용해 추출된 정보를 Supabase에 적용시켰다.

Next.js FE

supabase로부터 데이터를 받아옵니다. 이건 일반적인 Next.js 애플리케이션과 다를 게 없다. 다만 문제는 이렇게 받아온 데이터를 어떻게 React로 렌더링을 할 것인가이다.

옵션 1. iframe으로 웹버전 notion을 그대로 띄우기

iframe을 이용하는 것인데, 이건 당연히 iframe이 isolated된 환경이기에 내부에 커스텀을 하거나 모바일 최적화를 한다거나 할 수가 없기 때문에 사용하지 않았다.

옵션 2. react-notion-x로 렌더링하기

공식에서 지원하는 렌더링 라이브러리가 따로 없기 때문에, 서드파티를 이용해야 한다. 대표적으로 react-notion-x가 있다. 이름이 비슷한 여러 라이브러리들이 있는데, react-notion-x가 가장 사용하기 좋았다.
react-notion-x에서는 recordMap 이라는 데이터형을 요구한다. 문제는 recordMap이 notion-client라는 또 다른 서드파티 라이브러리를 이용해야 추출이 가능하다는 것이다. (공식 API로 recordMap을 뽑아오는 옵션이 있길래 써봤는데, 원본 페이지랑 너무 다르게 렌더링 되어 도저히 쓰지 못할 정도라 아직까지는 그냥 notion-client 쓰는게 좋다.)
  • 정리
  1. 페이지들의 목록이나 메타데이터는 @notionhq/client(공식 라이브러리) 를 사용해 가져옴
  1. 페이지 내용을 가져오는 것은 notion-client로 가져옴
  1. 그렇게 가져온 페이지를 렌더링 하는 것은 것은 react-notion-x로
 
실제 코드로는 다음과 같습니다.
  • 페이지 내부 정보 추출 (recordMap)
import { NotionAPI } from "notion-client"; const api = new NotionAPI(); // ... const recordMap = (await api.getPage(pageId)) as ExtendedRecordMap;
 
  • 페이지 렌더링
import { NotionRenderer } from "react-notion-x"; //... <NotionRenderer recordMap={recordMap} // ... 기타 옵션들 (코드블럭 띄울거냐, 풀페이지 할거냐 등등) />

캐싱 구조

  • 페이지 콘텐츠 캐싱
페이지의 콘텐츠, 즉 recordMap은 supabase에 캐싱하기보다는 Next.js의 SSG로 바로 캐싱하는 것이 일종의 이중과세(?)를 막을 수 있고 당연히 중간단계가 줄어드니 지연시간도 작다. 문제는 SSG가 빌드타임에 생성되어 고정된다는 것이다. 나중에 노션에서 페이지 내용을 수정하면 FE에서 적용이 안된다. 그래서 주기적으로 Jenkins 스케줄러에서 포스트 내용 변경이 있을 경우 자동화된 redeploy를 진행중이다. Vercel을 사용중이기 때문에 Jenkins에서 Deploy Hook을 호출해 redeploy해 SSG 페이지를 재생성하고 있다.
근데 ISR을 쓰면 되는거 아니냐?
→ ISR도 좋은 방법이다. 문제는 접속률이 Sparse하다면 ISR을 쓰는 의미가 퇴색된다. 왜냐면 Vercel에서 아직 유효기간 남아있는 경우에도 장기간 접속자가 없으면 그냥 Eviction 시켜버리기 때문이다. Eviction을 방지하기 위해 스케줄려로 주기적으로 페이지를 방문해주는 것도 방법이겠지만, 차라리 그렇게 할 바에는 redeploy 해서 고정적인 SSG를 생성하는 것이 좋다. 혹은 revalidatePath로 revalidate를 트리거링 해주는 것도 좋다.
 
  • 페이지 메타데이터 캐싱
페이지의 메타데이터, 페이지들의 목록은 supabase에 캐싱을 하고 있습니다. 중간에 DB를 넣지 말고 redeploy될 때 같이 박아놓으면 되는거 아니냐는 의문도 있을 수 있습니다.
굳이 왜 DB가 필요하냐?
1. 검색기능을 넣을때 supabase에서 쿼리문으로 뽑아오는 것이 좋기 때문입니다.
2. 서버리스 function의 메모리를 줄이기 위해 필요합니다. (메타데이터가 단순 텍스트만 있다면 문제될 것은 크게 없지만, 썸네일같은 용량 큰 데이터들 때문에 DB와 Bucket에 넣는게 좋습니다)
 

여담

옛날에는 Github Action을 스케줄러로 Git 코드베이스에 카테고리 데이터 + 페이지 데이터 모두 JSON으로 올려놓는 방식을 사용했었는데, 이러저러 단점이 너무 많아서 현재 설정으로 왔습니다.
제목: 셈틀 블로그는 어떻게 만들어졌나 (노션)작성일: 2025. 12. 26.