노션에 글을 쓰는건 좋아하지만, 기존 블로그는 MDX 파일로 관리되고 있었기에 매우 안좋은 글 작성 경험을 갖고 있었다.
기존 방식은 다음과 같은 게시물 작성 파이프라인을 거친다.
노션에 작성 → 로컬 mdx 파일로 옮기기 → git push → 빌드
노션에 이미 작성된 글을 다시 mdx 파일로 재작성 하는 부분이 제일 번거로웠다. 더군다나 게시물에 이미지를 첨부해야 하는 경우엔, 노션에서 이미지를 다운받고 로컬에 이미지 파일을 추가한 뒤, mdx파일에서 이를 불러와야 했다. 이런 귀찮은 게시물 작성 파이프라인 때문에 블로그 글을 쓰는게 꺼려졌다.
그러나, 블로그 작성은 여러모로 필요했기에 이 부분을 개선하고자 마음을 먹었다.
어느 방식으로 할까?
다음 2가지 방법을 고려해보았다.
- Supabase로 게시물 관리하기
- Notion DB와 연동해서 게시물 관리하기
Supabase도 충분히 좋은 선택지라고 생각했다. DB를 직접 다룰 수 있으니 확장성도 좋고, Velog처럼 텍스트 에디터 기능도 구현하여 내가 게시물을 작성하는 기능도 구현할 수 있고 다양한 도전들을 할 수 있다고 생각했다.
그러나 블로그 글쓰기 경험을 개선하기 위한 해결책으로는 적합하지 않았다. 텍스트 에디터 기능을 구현하기 전까지의 작업 순서는 '노션 작성 → Supabase 열어서 복사 붙여넣기'가 된다. 기존에 '노션 작성 → mdx 파일 열어서 복사 붙여넣기' 하던 거랑 본질적으로 같은 파이프라인을 갖게 된다. 즉, mdx로 관리할 때랑 달라지는게 없다.
반면에 Notion DB를 연동하는 방식은 내가 겪던 문제의 근본 원인을 정확히 해결해주는 방법이다. 더 이상 노션을 '초안용'으로만 쓰는 게 아니라, 노션 자체를 내 블로그의 CMS(콘텐츠 관리 시스템)으로 거듭나게 한다.
기존: 노션에 글쓰기 → mdx 파일에 복붙 → 이미지 일일이 다운로드 → 로컬에 이미지 추가 → git push변경 후: 노션에 글 작성 → 발행 상태 체크 → 끝
특히 제일 번거로웠던 이미지 처리가 깔끔하게 개선이 됐다.
노션에서 이미지를 다운받고,
public/images
폴더에 넣고, mdx에서 경로를 ../../images/blah.png
이런 식으로 맞춰주는 고통스러운 짓을 안 해도 된다. 그냥 노션 본문에 이미지를 툭 던져놓으면, 알아서 그 이미지 주소를 가져와 블로그에 보여준다.따라서 내 작업 흐름에서 가장 마찰이 심하고 번거로운 부분을 정확히 제거해주는 Notion DB 연동 방식으로 개선하고자 했다.
사용할 라이브러리
일단 노션 페이지 데이터를 어떻게 파싱해서 보여줄지를 정해야했다.
- react-notion-x
- notion-to-md
react-notion-x는 노션의 비공식 API를 사용하여 마치 실제 노션 페이지에서 데이터를 받아오는 것과 같은 방식으로 데이터를 받아온다. 이로 인해 노션에 작성한 양식 그대로 가져올 수 있다. 반면에 notion-to-md는 노션 공식 API를 활용하며, 노션 API 문서(
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...
사실 둘 다 기본적인 마크다운들은 파싱하는데 훌륭하다. 그러나 차이점은 바로 “이미지 렌더링”에 있다.
비공식 API를 사용하면 이미지의 사이즈 정보를 가져올 수 있다. 따라서 다음과 같이 노션에서 구성한 이미지 레이아웃을 그대로 렌더링할 수 있다는 장점이 있다.

그러나 노션 공식 API에선 이미지의 사이즈 정보를 가져올 수 없다. 이로 인해 이미지들이 단순히 나열된다. 나는 부분에서 react-notion-x 라이브러리가 매우 마음에 들었다.
더군다나 notion-to-md는 노션 공식 API를 사용하기에 노션 API 제한(
Notion APIRequest limits.%20The%20rate%20limit%20for%20incoming%20requests%20per%20integration%20...&logoUrl=https%3A%2F%2Ffiles.readme.io%2Fa267aac-notion-devs-logo.svg&color=%23fffefc)
)에 걸릴 수 있다.
Request limits
To ensure a consistent developer experience for all API users, the Notion API is rate limited and basic size limits apply to request parameters. Rate limits Rate-limited requests will return a "rate_limited" error code (HTTP response status 429). The rate limit for incoming requests per integration ...
하지만 react-notion-x 가 장점만 있는건 아니다. 비공식 API를 사용하다 보니 API가 변경되면 그 즉시 깨질 수 있는 위험이 존재한다.
그래도 꾸준히 잘 관리가 되고 있고 API 변경에 따른 수정도 즉각적으로 이루어지고 있음을 확인하여, react-notion-x를 이용하기로 결정했다.
노션 API 세팅
나는 다음 블로그를 참고하여 세팅했다. 정말 친절히 다 알려준다.
ISR 적용
ISR이란, 미리 빌드한 결과물만 보여줌으로서 빠른 접근이 가능한 SSG에 실시간 변경 사항을 반영하는 SSR의 장점을 합친 방식이다.
Guides: ISR
Guides: ISR
Learn how to create or update static pages at runtime with Incremental Static Regeneration.
이 방식을 활용하면 변경사항을 반영할때 전체 앱을 빌드하지 않고, 변경한 부분만 선택하여 백그라운드에서 빌드할 수 있다.
ISR에는 2가지 전략이 있다.
- Time Based Revalidation: 특정 주기 마다 페이지를 자동 갱신함
- On Demand Revalidation: revalidatePath 또는 revalidateTag 함수를 사용하여 특정 이벤트가 발생했을 때만 갱신함
나는 Notion을 CMS로 사용하고 있어, 글이 업데이트 될 때마다 즉시 변경사항을 블로그에 반영하고 싶었다. 따라서 On Deman 방식으로, 그 중에서도 경로 기반인 revalidatePath를 선택했다.
revalidateTag도 가능하지만, 현재 /posts/[slug]와 같은 라우팅 구조를 갖고 있기에 reavalidatePath만으로도 충분히 효과적이라고 생각했다.
Notion 웹 훅과 연동
실제 Notion 웹훅을 연동하기 위해선 위에서 등록한 Notion API에 들어가서 ‘웹훅’ 탭을 클릭해준다. 그러면 웹훅 URL을 확인할 수 있는데, 노션 웹훅이 해당 URL로 갱신 정보를 보내준다.
나는 다음과 같이 구성했기에 프로젝트에 /app/api/revalidate/route.ts 파일을 추가해주어야 한다.
그리고 또 중요한 점이 secret도 설정을 해주어야 하는데, secret을 설정해주지 않으면, 제 3자가 해당 URL로 잘못된 요청을 보내 우리의 서비스를 엉망으로 만들 수 있기 때문이다.

실제 코드를 보면서 살펴보자. 요청의 secret과 내가 정의한 secret이 맞는 경우에만 유효한 요청을 간주한다.
export async function POST(request: NextRequest) { try { const body = await request.json(); // Handle revalidation requests const secret = request.nextUrl.searchParams.get("secret"); if (secret !== process.env.REVALIDATION_SECRET) { return NextResponse.json({ message: "Invalid secret" }, { status: 401 }); }
Notion에서 글이 변경되었다는 웹훅 요청을 받으면, revalidatePath 함수를 호출하여 관련 페이지들의 캐시를 무효화한다. 이를 통해 페이지 갱신을 진행한다.
const pageId = body.entity.id; console.log("Notion Page ID from Webhook:", pageId); // Assuming the webhook payload from Notion indicates a change, // we revalidate the 'posts' path. revalidatePath("/", "page"); revalidatePath("/posts", "page"); revalidatePath(`/posts/${pageId}`, "page");
revalidatePath가 호출되면 Next.js는 해당 경로의 캐시된 페이지를 즉시 무효화한다. 그리고 다음 사용자가 해당 페이지에 접속했을때 서버는 백그라운드에서 페이지를 새로 생성하여 최신 버젼으로 캐시를 업데이트한다.
후기
이후 다룰 트러블 슈팅기에도 다루겠지만, 정말 다사다난 했다.. 그러나 기존 최악의 글 작성 경험을 개선하여 지금은 맨날 글을 써도 무리가 없는 정도이다ㅎㅎ. 이렇게 실제 문제를 해결해 나가는게 바로 개발자의 낙 아닐까??