
들어가며
저는 Github page로 배포한 개인 블로그를 운영하고 있습니다. 이 글에서는 Notion API를 활용해 어떻게 정적 페이지를 배포했는지 그 과정과 경험을 공유하고자 합니다.
1️⃣ SSR을 사용하지 않고, SSG를 사용하는 이유?
SSR을 이용해서 Vercel로 배포하는 건 오버엔지니어링이라고 생각했습니다. 블로그를 만드는데, 동적 호스팅 서버가 왜 필요한지에 대해 고민했고, 결론은 "필요없다"였습니다.
Vercel 대신 Github pages를 선택했고, 이 과정에서 많은 시행착오를 겪었습니다. 특히 외부 라이브러리를 사용할 때 인증과 보안 문제가 가장 심각했으며, CSR(클라이언트 사이드 렌더링)의 한계에 대해서도 직접 경험할 수 있었습니다.
2️⃣ Notion 페이지를 어떤 방식으로 렌더링 했을까?
각설하고, 제가 Notion 페이지를 어떻게 구현했는지 살펴보겠습니다.
제가 사용한 도구는 다음과 같습니다.
notion-sdk-js
makenotion • Updated Apr 29, 2025
위 도구를 통해서 각 page의 id값을 불러올 수 있습니다.
import { Client } from "@notionhq/client"; const notionClient = new Client({ auth: process.env.NOTION_SECRET, }); const result = await notionOfficialClient.databases.query({ database_id, }); //result에는 database내의 page id값을 알 수 있다.
- NOTION_SECRET값은 아래 노션으로 설정한 API Integration을 사용하여 설정할 수 있습니다.
react-notion-x
NotionX • Updated Apr 29, 2025
@notionhq/client
도구를 통해 받아온 pageId값을 통해서 각 페이지의 recordMap을 받아옵니다.import { NotionAPI } from "notion-client"; const notionClient = new NotionAPI(); const recordMap = await notionClient.getPage(pageId);

이렇게 노션 페이지 아래부분을 Collection이라고 하는데, 이를 통해서 SEO향상을 위한 metadata를 설정할 수 있었습니다.
import { getPageProperty } from "notion-utils"; export const notionClient = new NotionAPI({ authToken: process.env.NOTION_TOKEN_V2, activeUser: process.env.NOTION_USER_ID, }); // Collection 데이터를 불러오기 위해선 인증이 필요하다. //... const title = getPageTitle(recordMap); const description = getPageProperty("설명", recordMap.block[pageId]?.value, recordMap);
제목과 collection 값을 이렇게 불러올 수 있었습니다. 참고로 추가 인증 값이 필요합니다.
렌더링은 놀라울 정도로 간단합니다.
import "react-notion-x/src/styles.css"; import "prismjs/themes/prism-tomorrow.css"; import "katex/dist/katex.min.css"; import { NotionRenderer } from "react-notion-x"; //... <NotionRenderer disableHeader recordMap={recordMap} fullPage={true} darkMode={false} components={{ // 원하는 컴포넌트로 Custom 가능. }} />
CSS 스타일을 적용시켜주고, NotionRenderer를 이용해 렌더링만 해준다면, Notion 페이지와 동일한 모습을 볼 수 있습니다! 정말 간단하게 Notion 페이지를 가져올 수 있게 되는 것입니다.
3️⃣ 구현하는 과정에서 만났던 챌린지들
react-notion-x
는 private page의 정보를 가져오도록 지원합니다.export const notionClient = new NotionAPI({ authToken: process.env.NOTION_TOKEN_V2, activeUser: process.env.NOTION_USER_ID, });
이런식으로 데이터를 불러오는 것까진 좋았습니다. 하지만 모든 이미지가 제대로 렌더링 되지 않았습니다. 400에러가 발생했습니다.

개발자 도구 네트워크 탭을 켜서 디버깅을 해봐도, 별다른 에러 메세지를 제공해주지 않아 원인을 찾을 수 없었고, react-notion-x의 이슈를 통해서 해결의 실마리를 찾았습니다.
File is not working(ex: music, video)
위 이슈를 살펴보고, signedUrls을 사용해야 한다고 판단했습니다.
<NotionRenderer mapImageUrl={(url, block) => { const signedUrl = recordMap.signed_urls?.[block.id]; return signedUrl || url; }} />
이미지 URL을 위와 같이 수정했습니다. 실제로도 요청이 달라지는 것을 확인할 수 있었습니다.

요청 URL이 바뀜에 따라 에러도 변경되었습니다. 403 에러는 인증 에러입니다.
로그인은 되어있지만, 접근 권한이 없는 문제였기 때문에 원인을 파악하기 쉽지 않았습니다. 다음 이슈에서 힌트를 발견했습니다.
Images from Signed URL returns 403
SignedURL을 통해 파일에 접근하기 위해선 file_token 쿠키 설정이 필요하다는 것을 알게 되었습니다.
하지만 여기서 저는 Private Page 배포는 중단하기로 결정했습니다.
SSG 환경이다 보니 이미지를 불러오기 위해서는 토큰 값이 클라이언트에 노출될 수밖에 없었기 때문입니다.
4️⃣ 마지막으로.. 정적 페이지 배포에 대한 소회
요즘 개발 트렌드는 Vercel이나 Netlify 같은 동적 호스팅 서비스로 기울어져 있습니다. 하지만 이런 선택이 항상 최적일까요?
Kent C. Dodds가 지적했듯이 "가장 좋은 성능의 서버는 필요 없는 서버다."라는 원칙을 되새길 필요가 있습니다. 블로그와 같은 콘텐츠에서 과도한 인프라는 단순히 비용 증가로 이어질 뿐입니다.
React 창시자 Dan Abramov도 "모든 프로젝트에 SSR이 필요한 것은 아니다. 가끔은 정적 HTML만으로도 충분하다"라고 말한 바 있고, Rich Harris(Svelte 개발자)는 "우리는 종종 도구의 복잡성을 그 가치와 혼동한다"고 경고했습니다. Github Pages와 같은 단순한 정적 호스팅으로도 블로그를 효과적으로 운영할 수 있으며, 이는 Next.js 공동창업자 Guillermo Rauch도 "적절한 도구를 적절한 문제에 사용하라"는 원칙으로 강조한 부분입니다.
이런 관점에서 제 Github Pages 기반 정적 블로그는 비용 효율성뿐만 아니라, SSG, SSR, ISR에 대한 더 깊은 이해로 이어진 현명한 선택이었다고 생각합니다. 위와 같은 삽질 과정이 사실 SSR이라면 손쉽게 해결되는 부분도 분명 있었겠지만, 이런 과정을 통해서 저는 프론트와 백엔드 사이의 상호작용에서 어떤 일이 일어나는지 깊게 이해할 수 있는 경험이 되었습니다.
결국 "필요한 기술만 적절히 사용하기"라는 철학이 더 깊은 기술 이해로 이어진 아이러니한 경험이었습니다.