logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    react-notion-x: notion 페이지를 react에 그리기

    이미지 보기

    react-notion-x: notion 페이지를 react에 그리기

    notion 페이지 컨텐츠를 page id만으로 react에서 렌더링할 수 있어요

    • 25.07.28 작성

    • 읽는 데 25

    TOC

    들어가며

    작성 배경

    회사 사이트에서 블로그 개발을 고려하고 있는데, 원래는 에디터부터 컨텐츠 DB까지 모든 데이터와 UI 흐름을 직접 개발하려 했습니다. 고성능 WYSIWIG 에디터 개발이 가장 큰 공수가 예상되었는데, 우선순위가 더 높은 개발 사항이 있어 손이 부족했습니다.

    이에 따라 inblog라는 외부 블로그 솔루션을 사용하는 것이 안건으로 나온 와중에, 자체 개발의 끈을 놓지 말자는 의견이 추가되었고, react와 notion을 연동하는 react-notion-x가 언급되어 사용성을 더 알아보고자 했습니다.

    라이브러리를 본격적으로 R&D하고 기술적 검토를 진행하여 실무에 녹이기 전의 작업 계획서 개념으로 글을 작성했습니다. 이 라이브러리를 본격적으로 도입한다면, 어떤 기능들을 만족해야 하고 어떤 게 우려가 되는지, 그리고 이 우려점들을 라이브러리가 어떻게 해결할 수 있을지 검토할 예정입니다.

    핵심 주제

    • 라이브러리의 기능에 대한 폭넓은 이해
    • 서비스에서 요구되는 기능, 요구되리라 예상되는 기능들을 라이브러리가 만족하는지 확인
    • 프로그래밍 영역에서 녹이는 절차(라이브러리 도입이 확실시 된다면)
    • 아키텍쳐 및 개발 흐름 설계

    조사 전

    필요 및 확인해야 할 기능

    • data or data & ui?
      • notion api처럼 notion page의 데이터만 가져오는 개념인지
      • iframe처럼 notion data + UI를 그대로 컴포넌트에 렌더링하는 개념인지
    • 컨텐츠 로딩 및 렌더링 속도
    • url customizing 기능 여부
      • page id 방식으로 page 불러온다고 하더라도 url path는 자체적으로 구성할 수 있어야 함
    • HTML import/export
      • 라이브러리보다는 notion 자체 기능 검토
      • import: 기존 DB의 html code를 notion으로 옮길 수 있을지
      • export: notion에 누적한 컨텐츠를 추후에 html 등으로 내부 DB에 옮길 수 있을지
    • UI 및 스타일 재구성 가능성
      • notiontic한 UI를 서비스에 맞게 customizing 할 수 있을지

    장단점 포인트

    image 등 File Upload시 Notion CDN 사용

    장점

    • 별도의 storage, UI 고도화 없이 Notion UI, Storage를 그대로 활용이 가능하다.

    단점

    • HTML 코드로 내부 DB에 저장하는 방식으로 migration하는 경우 file을 내부 DB에 올리고 html 속 file url을 바꿔줘야 할 수 있다.

    Notion Editor 사용

    장점

    • 텍스트, 이미지, 레이아웃 등 에디팅 UX 및 완성형 UI 제공
    • code block 사용 가능하여 tech blog 확장에도 이점 예상
    • 비개발 직군의 컨텐츠 생산에 있어 개발자에 의존하지 않아도 컨텐츠 확장 및 수정 가능

    단점

    • 텍스트 크기, 색상, 하이라이트 등 디테일한 설정 불가능
    • 노션에서 기본 제공하는 color pallete 내에서 가능(Color Rainbow)

    react-notion-x

    react-notion-x는 이런 라이브러리입니다.

    • notion 데이터를 빠르고, 정확하게 rendering하는 renderer를 제공
    • notion을 CMS처럼 사용하고자 할 때 사용
    • react-notion에서 시작했으나 더 많은 type의 block을 지원
    • react 뿐만 아니라 next.js 환경의 예시 코드와 sample kit 프로젝트 제공

    사용법은 밑에서 더 다뤄보겠습니다.

    Next.js Starter Kit

    위에서 언급한 Next.js 환경에서 react-notion-x를 사용하는 sample kit 프로젝트입니다. Next.js를 사용하고 있는 제 상황과 딱 맞습니다. 이 프로젝트를 뜯어보면서 react-notion-x의 기능과 사용 노하우를 얻어보겠습니다.

    starter kit 프로젝트 README 및 코드 파악 수준이므로 자세한 이미지와 개발 코드는 PoC 과정 부분을 참고해주세요.

    주의사항

    • 초점 : 위에서 언급한 우려사항 or 필요 기능을 만족하는 기능들에 초점을 맞췄습니다.
    • 주의점 : package.jsonnext 버전은 ^15.3.3이지만, app router가 아니라, next13 이전의 page router 버전으로 코드가 짜여져 있습니다. app router를 사용하고 있다면 흐름을 파악하는 용도로만 코드를 참고하되, 큰 코드는 사용하지 못한다는 점에 주의해주세요.

    스타일링

    • notion ui block의 스타일을 커스텀할 수 있습니다.
    • 기본적으로 react-notion-x 라이브러리에서는 notion style을 css로 정의하고 import할 수 있게 합니다.
    • 하지만 css override를 통해 custom ui style을 설정할 수 있습니다.

    ToC

    기타

    nextjs-notion-starter-kit에는 아래의 더 많은 기능들이 있지만, 샘플 프로젝트 자체 설정이거나 제 활용 범위에 직접적인 관련이 없어 언급만 하고 넘어갑니다.

    PoC 과정

    next@15.4, react@19.1, 패키지 매니저는 pnpm을 사용하여 실습을 진행합니다.

    준비물

    public한 notion page를 하나 준비해야 합니다. 저는 이런 페이지를 하나 만들었습니다. 페이지 id는 23cc5c672f9e80f5864cdbc3a2ffc0a6입니다.

    notion page

    기본 컨텐츠 렌더링

    공식문서 Usage대로 해보기 위해 아래의 기본 의존성을 먼저 설치합니다.

    pnpm install notion-client react-notion-x notion-types
    
    • notion-client : notion api를 통해 페이지 컨텐츠를 불러오기 위한 패키지
    • react-notion-x : raw notion data를 rendering하는 renderer 및 기타 기능 제공
    • notion-types : typescript 친화적인 개발 코드를 위한 별도 지원하는 패키지

    먼저 Usage를 참고해 이런 컴포넌트를 임시로 만들어 렌더링만 테스트해봤습니다.

    // NotionPage.tsx
    
    import { NotionAPI } from "notion-client";
    import { NotionRenderer } from "react-notion-x";
    
    const TEST_NOTION_PAGE_ID = "23cc5c672f9e80f5864cdbc3a2ffc0a6";
    const NotionPage = async () => {
      const notion = new NotionAPI();
      const recordMap = await notion.getPage(TEST_NOTION_PAGE_ID);
    
      return (
        <NotionRenderer recordMap={recordMap} fullPage={true} darkMode={false} />
      );
    };
    
    export default NotionPage;
    
    // page.tsx
    "use client";
    
    import NotionPage from "@/components/NotionPage";
    
    export default function Home() {
      return <NotionPage />;
    }
    

    이렇게 하니 UI는 나오지만 이유를 알 수 없는 무한 chunk 호출 오류가 있었습니다.

    notion page

    이러한 서버 컴포넌트 - 클라이언트 컴포넌트 분리로 해결할 수 있었습니다.

    // NotionPage.tsx
    import { NotionAPI } from "notion-client";
    import NotionRendererClient from "./NotionRendererClient";
    
    const TEST_NOTION_PAGE_ID = "23cc5c672f9e80f5864cdbc3a2ffc0a6";
    
    const NotionPage = async () => {
      const notion = new NotionAPI();
      const recordMap = await notion.getPage(TEST_NOTION_PAGE_ID);
    
      return <NotionRendererClient recordMap={recordMap} />;
    };
    
    export default NotionPage;
    
    // NotionRendererClient.tsx
    "use client";
    
    import { NotionRenderer } from "react-notion-x";
    import type { ExtendedRecordMap } from "notion-types";
    
    interface NotionRendererClientProps {
      recordMap: ExtendedRecordMap;
    }
    
    const NotionRendererClient = ({ recordMap }: NotionRendererClientProps) => {
      return (
        <NotionRenderer
          recordMap={recordMap}
          fullPage={true}
          darkMode={false}
        />
      );
    };
    
    export default NotionRendererClient;
    

    이런 결론을 낼 수 있었습니다.

    • react-notion-xNotionRenderer 컴포넌트는 client component에서만 사용 가능하다.
    • notion-client의 NotionAPI는 server component에서 호출해야 문제가 없다.
    • 때문에 server component에서 NotionAPI로 데이터(recordMap)를 불러오고 이를 client component에 props로 넘긴다.

    문제를 해결하고 README Packages을 다시 보니 이런 설명이 있었습니다.

    notion page

    notion-client 패키지의 Notion API는 CORS 정책에 의해 server component에서만 호출되어야 한다는 rule이 있었나 봅니다. 공식문서 잘 보기...ㅎㅎ

    아무튼 결론적으로 이런 모습의 페이지가 나왔습니다. 데이터는 잘 불러오는 듯 하네요.

    notion page

    Styles

    흐린 눈을 하고 넘어간 것들이 있습니다. 데이터는 불러오지만 디자인이 영 별로입니다. 왜 notion처럼 안 나오는 걸까요? 이는 공식문서 - styles에서 솔루션을 얻을 수 있었습니다.

    // core styles shared by all of react-notion-x (required)
    import 'react-notion-x/src/styles.css'
    
    // used for code syntax highlighting (optional)
    import 'prismjs/themes/prism-tomorrow.css'
    
    // used for rendering equations (optional)
    import 'katex/dist/katex.min.css'
    

    이를 위해 추가 의존성을 설치해줬습니다.

    pnpm install prismjs katex
    

    react-notion-x에서 기본 제공하는 css 파일인 import 'react-notion-x/src/styles.css'을 딱 하나만 추가해도 많은 스타일이 notion에 가깝게 바뀌는 것을 확인할 수 있습니다. (왜 제목이 2개씩 렌더링되나 했더니 header navigation이었나 보네요.)

    notion page

    아쉽게도 code block은 여전히 안 나오고, primsjs와 katex 설치의 의미는 아직 찾지 못했습니다. (하지만 곧 쓸모가 있어집니다.)

    optional components

    위에서 제대로 안 나오던 금쪽이 컴포넌트들만 모아봤습니다. 이상하게 나오거나 아예 안 나오는 친구들이죠.

    notion page
    notion page

    이 친구들은 별도의 components 옵션으로 third-party component를 넘겨줘야 합니다.

    The default imports from react-notion-x strive to be as lightweight as possible. Most blocks will render just fine, but some larger blocks like PDFs and collection views (database views) are not included by default.

    쉽게 해석하면 “가볍게 유지하기 위해서 큰 블럭들은 기본적으로 포함 안 했으니 안 보일 거다, 쓰고 싶으면 직접 넣어라.”라는 의미입니다.

    next.js 사용 예시를 주기도 하고, starter kit project에서도 실제 사용 예시가 있기 때문에 이를 참고하여 시범 코드를 작성해보았습니다. next/dynamic를 사용하여 lazy loading을 고려한 성능적으로 좋은 코드 예시입니다.

    // NotionCode.tsx
    import dynamic from "next/dynamic";
    
    const NotionCode = dynamic(() =>
      import('react-notion-x/build/third-party/code').then(async (m) => {
        // add / remove any prism syntaxes here
        await Promise.allSettled([
          // @ts-expect-error Ignore prisma types
          import('prismjs/components/prism-markup-templating.js'),
          ...
          // @ts-expect-error Ignore prisma types
          import('prismjs/components/prism-yaml.js')
        ])
        return m.Code
      })
    )
    

    Code block만 이렇게 보였지만, 다른 컴포넌트들도 비슷하게 처리해주면 됩니다. 필요하다면 commit 코드를 참고해주세요.

    다음은 index.ts로 모아서 export 하겠습니다.

    // notion-components/index.ts
    
    export * from "./NotionCode";
    export * from "./NotionCollection";
    export * from "./NotionEquation";
    export * from "./NotionModal";
    export * from "./NotionPdf";
    

    실제 사용부인 NotionRendererClient.tsx에서는 이렇게 사용합니다.

    // NotionRendererClient.tsx
    ...
    import {
      NotionCode,
      NotionCollection,
      NotionEquation,
      NotionModal,
      NotionPdf,
    } from "./notion-components";
    
    const NotionRendererClient = (...) => {
      const components = useMemo<Partial<NotionComponents>>(
        () => ({
          nextImage: Image,
          nextLink: Link,
          Code: NotionCode,
          Collection: NotionCollection,
          Equation: NotionEquation,
          Pdf: NotionPdf,
          Modal: NotionModal,
        }),
        []
      );
    
      return (
        <NotionRenderer
          ...
          components={components}
        />
      );
    };
    
    ...
    

    이미 코드가 많습니다만 많이 함축했습니다. 큰 흐름의 소개를 위한 1flow 코드입니다. 이번 작업의 commit file change를 첨부하니 참고하길 바랍니다.(링크)

    이런 결과물을 얻었습니다.

    notion page

    파일은 이게 최선인가 싶긴 하지만, code block과 equation은 정상적으로 표현되는 것을 볼 수 있습니다.

    ToC

    table of content는 보통 이런 기능을 가지고 notion toc도 마찬가지입니다.

    • 페이지 내의 heading들이 어떤 게 있는지 보여준다.
    • 현재 페이지에서 위치한 heading에 highlight가 된다.
    • 해당 heading 위치로 바로 focus될 수 있게 한다.

    이 toc를 표시하기 위해서는 이렇게 하면 됩니다.

    <NotionRenderer
      ...
      showTableOfContents={true}
    />
    
    notion page

    다만 이런 문제들이 있었습니다.

    • 화면 해상도가 높을 때만 보여집니다. 필요하다면 notion-aside css 속성을 수정하여 고도화할 수 있습니다.
    • heading1을 클릭했는데 heading2가 highlight되고, heading2를 클릭했는데 heading3이 highlight됩니다. focus는 정상적으로 되는 걸 보니 예시 페이지에서 heading간 거리가 너무 가까워서 생긴 문제인 듯 하네요.

    Style Customization

    가장 중요한 포인트입니다. notion의 UI를 사용할 수 있어서 좋지만, 한편으로는 너무 ‘notiontic’한 UI는 기성 서비스에서 아쉬움을 줄 수 있습니다. 사이트의 톤앤매너를 이어가려면 스타일 커스텀이 필요합니다.

    다행히 sample project에서 style override에 대해 다룬 부분이 있습니다. 이를 참고해 스타일 커스텀 PoC를 진행해보겠습니다. 어렵지 않습니다. notion renderer에서 사용하는 classname 예약어에 대해 별도 css 스타일을 주면 됩니다.

    /* styles/notion.css */
    
    .notion-text {
      color: red;
    }
    
    // NotionRendererClient.tsx
    
    import "react-notion-x/src/styles.css";
    ...
    import "@/styles/notion.css";
    

    class에 해당하는 일반 텍스트들은 모두 red 색상으로 처리되었습니다.

    notion page

    위에는 기능 소개를 위한 간단한 예시였지만 실제로는 이렇게 사용할 수 있겠네요.

    /* styles/notion.css */
    
    .notion-table-of-contents-active-item .notion-table-of-contents-item-body {
      font-weight: bold;
      color: rgb(51, 200, 43);
    }
    
    notion page

    기타 자세한 class 예약어 확인은 starter-kit의 notion.css (링크)를 참고하면 좋습니다. 물론 브라우저 개발자 도구에서 직접 확인해도 됩니다.

    정적 url paths

    블로그를 운영할 때 notion으로 작성한 페이지마다 id가 그대로 url에 노출되면 곤란하겠죠. 컨텐츠 페이지마다 페이지의 내용이 잘 드러나는 url path를 사용하는 것이 좋습니다.

    이를 위해 starter-kit 프로젝트에서는 site.config.ts 에서 정적 page id와 path의 map을 설정하게 하고 있습니다. (링크) 이 pageUrlOverrides 객체는 서비스 전반에서 next path로부터 page id를 찾아오는 역할을 하고 있습니다.

    // site.config.ts
    
    pageUrlOverrides: {
      '/foo': '067dd719a912471ea9a3ac10710e7fdf',
      '/bar': '0be6efce9daf42688f65c76b89f8eb27'
    }
    

    다만, 정적으로 등록을 하는 방식이기 때문에 개발자 없이 CMS처럼 컨텐츠를 확장할 수는 없는 구조입니다. 때문에 상용 서비스에서의 유연한 사용을 위해서는 별도 DB와 아키텍처 구성이 필요합니다.

    상용 서비스로의 적용

    아키텍쳐 설계

    제한점과 방향성

    • starter kit에서는 notion Database gallery view를 통해 grid card view 목록을 구현했습니다.
    • 그런데 실제 상용 서비스에서는 카테고리별 filter view, 검색어 filter 등 고도화된 기능이 필요하고 notion gallery를 사용한다면 이는 불가능합니다.
    • 때문에 page id로부터 내부 컨텐츠를 그리는 것은 react-notion-x에 맡기고, 목록 UI는 내부 DB에서 불러와서 그립니다.
    • 결론은 세부 페이지 렌더링은 제외한 나머지는 내부 서버에서 관리합니다. react-notion-xpage id → page 컨텐츠 그리기만 위임합니다.

    notion

    완전한 외부 의존성이므로 최대한 적은 역할을 위임합니다.

    • page별 컨텐츠를 작성하고 수정합니다.

    admin(CMS)

    통제 가능한 데이터를 최대한 CMS에서 처리합니다.

    • notion page id와 url path를 별도로 구성합니다.
    • category, tags, 게시 여부, 작성자, 썸네일 등 개별 컨텐츠 관리를 위한 필드를 추가합니다.
    • 검색, 정렬, 필터 등 관리자가 컨텐츠를 조회, 수정, 관리할 수 있도록 합니다.

    homepage

    목록은 CMS 데이터를, 세부 페이지는 react-notion-x의 WYSIWYG 렌더링을 사용합니다.

    • 목록 컨텐츠 UI를 그립니다.
    • 카테고리별, 태그별, 검색어별 필터링 기능을 추가합니다.
    • 썸네일 이미지를 표시합니다.
    • 썸네일 이미지를 클릭하면 세부 페이지로 이동합니다.
    • admin DB의 page id - url path 매핑 정보를 참고해 redirect합니다.
    • sitemap을 DB로부터 조회해 url path를 받아와 관리합니다.

    코드 구현과 미래

    • 코드 레벨 아키텍처를 그리지는 않았지만, 개념적 아키텍처 설계, 그리고 역할과 권한의 분리의 결정이 된 듯 합니다.
    • 일부 구현했고, 일부 구현이 남았지만 구현한 영역도 내부 코드 영역이 많아 코드 레벨로는 노출하지 않겠습니다.
    • 다만 이후에 기회가 된다면 정돈해 코드 레벨 아키텍처와 구현부를 일부 공개하겠습니다.
      • turborepo의 /packages/lib/react-notion-x 영역에 NotionPage renderer 패키지 모듈을 구현했습니다.
      • notion-types 패키지를 사용하여 props의 typescript 타입을 최대한 지원했습니다.
      • DB url path -> page id 매핑 정보를 조회하고 rendering하는 SSR 로직이 기술적 접근 중점이 될 예정입니다.
      • page permanent redirect와 dynamic sitemap 생성, canonical url 등 SEO 최적화도 고려해야 할테고요.

    우려 포인트

    상용 서비스로서 사용하기 위해 notion과 library의 제한점들을 조금 더 살펴보고 수준을 검증하겠습니다.

    notion import

    기존 DB에 저장중이던 html string을 notion editor → react-notion-x로 녹일 수 있을지 검토해보려 합니다.

    notion page
    notion page

    비교적 최신 컨텐츠는 원본에 비한다면 아주 못 봐줄 정도는 아닌 듯 하네요.

    하지만, 초기 html을 확인해보니 에디터가 달랐던 모양인지 많이 깨지는 것을 확인했습니다.

    notion page
    notion page

    마이그레이션을 한다면 조금 더 고려해야 하겠지만, 일단은 기존 에디터를 사용하는 것으로 결정했습니다.

    notion export

    notion에서는 html export 기능을 제공합니다. 하지만 export된 html 파일이 이미 notion에 상당히 많이 의존적이고 깔끔한 방식이 아니어서 tiptap editor 등 modern editor에 녹이기 힘들 것으로 보입니다.

    notion page

    notion editor에 컨텐츠를 많이 쌓는다면 내부 DB로의 마이그레이션은 점점 더 어려워질 것으로 예상됩니다. 내부 DB로 확장할 거라면 의사 결정을 빠르게 하는 것이 필요하겠네요.

    마치며

    더 하지 못한 내용

    NotionRenderer는 다양한 props를 가집니다. 설정할 수 있는 커스텀 옵션들이 참 많은데 이번 세션에서는 크게 중요하지 않아서 깊게 다루지 않았습니다. 정말 적용에 관심 있으시다면 공식 문서를 참고해주세요. 저도 기회가 된다면 props를 설정하는 가이드 포스트를 작성해보겠습니다. 긴 글 읽어주셔서 감사합니다.

    Reference

    profile

    FE Developer 박승훈

    노력하는 자는 즐기는 자를 이길 수 없다