현재 블로그는 Next.js로 구성되어 있다. Next.js는 SSG가 기본으로 적용된다. 그러나, 나는 노션에 글을 작성하면 이게 실시간으로 블로그에 반영이 되길 바랬다.
이를 구현하기 위해선 Next.js의 ISR(
Guides: ISR
)을 이해해야 한다. 이 부분은 현재 포스트에서 자세히 다루지 않겠다.
Guides: ISR
Learn how to create or update static pages at runtime with Incremental Static Regeneration.
왜 ISR이 제대로 이루어지지 않지?
나는 데이터베이스에 글을 추가하면 노션 웹 훅이 Vercel로 validation 요청을 보내고, 이 요청을 확인하여 revalidate를 호출하여 ISR을 진행하도록 구성했다.


그러나 이 기능이 제대로 작동되지 않았다. ISR을 진행하여 page 데이터를 받아오는데 Tiemout이 발생했다.

그러나 notion-to-md 라이브러리를 사용하면 정상적으로 ISR이 이루어지는데, react-notion-x를 사용하면 되지 않았다. 따라서 react-notion-x에 문제가 있다고 판단했고 그 속을 파보기로 결정했다.
react-notion-x
react-notion-x를 이용하여 notion 페이지 정보를 가져오기 위해 notionX의 클라이언트를 생성하고 getPage메서드로 특정 페이지의 정보를 가져오도록 코드를 작성했다.
const notionX = new NotionAPI(); ... export const getPostRecordMap = async (pageId: string) => { const recordMap = await notionX.getPage(pageId); return recordMap; };
getPage가 어떤식으로 구성이 되어있나 확인해보자. 내부적으로 노션의 비공식 API를 호출하여 데이터를 받아오는 형태이다.
export class NotionAPI { public async getPage(pageId: string): Promise<notion.ExtendedRecordMap> { const page = await this.getPageRaw(pageId) ... } public async getPageRaw(pageId: string) { ... return this.fetch<notion.PageChunk>({ endpoint: 'loadPageChunk', body, kyOptions }) } public async fetch<T>(...) { ... const res = await ky.post(url, { mode: 'no-cors', ...this._kyOptions, ...kyOptions, json: body, headers }) return ret.json(); } }
내가 주목한 부분은 바로 이곳이다.
npmnpm: ky
를 이용하여 post 요청을 보내고 있다. 현재 요청을 보냈지만 응답이 오지 않는 문제의 핵심 부분이라 생각했다.


npm: ky
Tiny and elegant HTTP client based on the Fetch API. Latest version: 1.10.0, last published: 7 days ago. Start using ky in your project by running `npm i ky`. There are 937 other projects in the npm registry using ky.
public async fetch<T>(...) { ... const res = await ky.post(url, { mode: 'no-cors', ...this._kyOptions, ...kyOptions, json: body, headers }) return ret.json(); }
mode: ‘no-cors’ 이 부분은 무얼 의미할까? 의미로 봐선 CORS를 적용하지 않는다는 의미인 것 같은데… 우선 CORS에 대해 간략히 짚고 넘어가자면, CORS는 Cross Origin Resource Sharing의 약자로 SOP 위에서 동작하는 보안 매커니즘이다. 서로다른 출처에 대해서 요청을 주고 받는 것을 관리하는 정책이라고 보면된다.

Request: mode property - Web APIs | MDN
The mode read-only property of the Request interface contains the mode of the request (e.g., cors, no-cors, same-origin, or navigate.) This is used to determine if cross-origin requests lead to valid responses, and which properties of the response are readable.
지금 상황이 딱 이런 상황이다. 요청은 보냈지만 그 처리가 제대로 이루어지지 않아 무한정 기다리는.. 이로 인해 Timeout이 발생하고 있다.
어떻게 해결할 수 있나?
요청이 제대로 오지 않는 문제에 대한 해결 방법 자체는 간단하다. NotionAPI 클라이언트 객체를 생성할때 ky options에 mode를 추가해주어 기존의 ‘no-cors’를 덮어씌우면 된다.
const notionX = new NotionAPI({ kyOptions: { mode: "cors", }, });
그래도 나는 아직도 의문이든다. CORS는 브라우저와 서버 통신간에 발생하는 메커니즘으로서 서버 간 통신에는 적용이 되지 않는다고 알고 있다.
어째서 ISR과정에서 일어나는 서버(Vercel) - 서버(Notion API) 통신에서 ‘no-cors’가 적용이 되고 있는걸까? 아니 실제로 ‘no-cors’가 적용되고 있는게 맞는걸까? 다른 원인이 있는건 아닐까?
서버 간 통신에서 정말 ‘no-cors’가 무시되나?
서버 간 통신에서 정말 CORS가 적용되지 않는지 먼저 확인할 필요가 있었다. 2개의 서로다른 출처를 갖는 서버를 로컬에 띄우고, 요청을 보내보았다.
import express from "express"; const app = express(); const port = 4000; app.post("/test-cors", (req, res) => { console.log("✅ /test-cors endpoint received a request."); // ⚠️ 중요: CORS 관련 헤더를 전혀 보내지 않음! // res.setHeader('Access-Control-Allow-Origin', '*'); // 이 코드가 없음 res.json({ message: "This is a secret message from the server!", success: true, }); }); app.listen(port, () => { console.log(`🚀 Test server listening at http://localhost:${port}`); });
import express from "express"; const app = express(); const port = 3000; app.get("/call-other-server", async (req, res) => { console.log( "🚀 클라이언트 서버(3000)에서 API 서버(4000)로 요청 보내는 중..." ); try { // 🔥 핵심: 서버에서 다른 서버로 요청 (CORS 헤더 없어도 OK!) const response = await fetch("http://localhost:4000/test-cors", { method: "POST", headers: { "Content-Type": "application/json", }, }); const data = await response.json(); console.log("✅✅✅ 서버 간 통신 성공! 받은 데이터:", data); res.json({ message: "서버 간 통신 성공!", fromOtherServer: data, proof: "CORS 헤더가 없어도 서버 간 통신은 문제없음!", }); } catch (error) { console.error("❌ 서버 간 통신 실패:", error); res.status(500).json({ error: error.message }); } }); app.listen(port, () => { console.log(`🌐 클라이언트 서버가 http://localhost:${port}에서 실행 중`); console.log(`📡 테스트: curl http://localhost:${port}/call-other-server`); });
서버 간 통신에 별도의 CORS 헤더를 설정해주지 않았음에도 불구하고 서버 간 통신이 잘 이루어진 것을 확인할 수 있었다. 고로 ‘서버 간 통신에는 CORS가 적용되지 않으며, mode: ‘no-cors’도 무효화된다.’가 증명됐다.


ISR이 안되는 ‘진짜’ 이유?
그렇다면 왜 우리의 상황은 mode: ‘no-cors’가 적용되고 있는 것과 같을까? 뭐가 문제인지 확인해보기 위해 fetch api와 ky로 Notion API를 호출하는 앱을 ISR로 빌드해보았다.
두 방식 모두 mode: ‘no-cors’로 설정하여 요청을 보내도록 했다.
if (fetchType === "ky") { response = await ky.post(url, { mode: "no-cors", headers, json: body, }); } else if (fetchType === "fetch") { response = await fetch(url, { mode: "no-cors", method: "POST", headers, body: JSON.stringify(body), }); }
결과는 놀라웠다.
fetch를 이용한 요청에 대해서는 ISR이 잘 이루어졌고, ky를 이용한 요청에 대해서는 기존과 동일한 문제가 발생했다. 즉, ky가 문제였다는 것이다?!!!!
=== ISR 페이지 생성 시작: fetch === 생성 시간: 2025-09-17T17:10:51.189Z 환경: Server (ISR) 🌐 Native Fetch API로 테스트 FETCH 환경: Server (ISR) -------- FETCH 요청 URL: https://www.notion.so/api/v3/loadPageChunk -------- FETCH 응답 타입: basic FETCH 응답 상태: 200 FETCH 응답 OK: true FETCH JSON 파싱 시도... FETCH JSON 파싱 성공 ✅ ISR 페이지 생성 성공: 285ms === ISR 페이지 생성 완료: fetch ===
=== ISR 페이지 생성 시작: ky === 생성 시간: 2025-09-17T15:07:54.134Z 환경: Server (ISR) KY 라이브러리로 테스트 KY 환경: Server (ISR) -------- KY 요청 URL: https://www.notion.so/api/v3/loadPageChunk -------- ❌ ISR 페이지 생성 실패 (5001ms): 5초 타임아웃: KY 요청 실패 === ISR 페이지 생성 완료: ky ===
그렇다면 어째서 ky를 이용한 요청이 제대로 처리가 되지 않는걸까?
문제의 본질은 WHATWG Fetch 표준(
패치 표준![패치 표준]()
)과 연결되며, 또한 Ky의 구현체와도 관련이 있다.
패치 표준
이 표준의 목표는 웹 플랫폼 전반에 걸쳐 패칭을 통합하고, 이에 관련된 모든 것에 대해 일관된 처리를 제공하는 것입니다. 여기에는 다음이 포함됩니다:
이 내용은 다음 글에서 더 자세히 다룬다.