Notion API를 사용한 게시물의 이미지 핸들링과 이미지 Expire time 에러 해결
현재 노션 블로그는 원래 서버 사이드 방식으로 랜더를 구현했다. 서버에서 Notion API를 사용하여 받은 데이터로 맵핑하기 쉽게 직접 구조트리로 변환 후 구현을 하다 보니 브라우저에서 긴 유휴시간이 발생하게 됐다.
너무 느린 초기 대기 시간으로 빠르게 보여줄 수 있는 다른 방법을 찾아야 했다.

먼저 generateStaticParams 메서드를 사용하여 빌드시 경로를 정적으로 웹 서버에 생성해보았다.
// app/(main)/blog/[id]
export async function generateStaticParams() {
const pageList = await getNotionPageList({
pages: 100,
});
return pageList?.results
? pageList?.results
.map((res) => res?.id)
.map((pageId) => ({
id: pageId,
}))
: [];
}
// utill/notions
if (block.type === 'image') {
return {
id: block.id,
type: 'image',
caption: block.image.caption,
...(block.image.type === 'file' ? { url: block.image.file.url } : {}),
};
}
새로운 게시물이 생성되면 빌드를 통해 업데이트를 시켜줘야 하는 비용과 웹 서버의 자원이 늘어난다는 단점들이 있지만 초기 응답 속도를 개선하기엔 최선의 방법이라고 생각하여 정적 생성 방식을 채택하기로 했다.
그런데 처음 배포를 했을 때와 다르게 몇 시간이 지나자 각 이미지에 “403 Forbidden” 에러가 발생하고 있는 걸 발견했다.


"Image": {
"id": "BlockId",
"type": "files",
"files": [
{
"name": "image_name.jpg",
"type": "file",
"file": {
"url": "https://s3.us-west-2.amazonaws.com/secure.notion-static.com
some_image_jpg.jpg",
"expiry_time": "2023-07-06T09:42:50.223Z"
}
}
]
},
그 이유는 Notion에서 DB나 페이지를 쿼리할 때마다 새로운 공개 URL을 생성하게 되는데 각 URL은 만료시간이 있어 expiry time(1시간)이 지나면 참조 할 수 없기에 정적으로 참조하는 방식을 권하지 않고 있다.

이렇게 Notion API에서 치명적인 오류가 발생하여 다시 원점으로 돌아가게 되었다.
먼저 기존 SSR 방식으로 돌아가 구조 트리를 만드는 과정을 최적화 시켜 유휴시간을 최소한으로 만드는 방법과 정적 페이지 방식을 고수하여 빌드 시 이미지들을 다운로드 후 특정 스토리지에 저장을 한 다음 해당 파일을 조회하는 방법으로 초기 랜더링을 잡기위한 두 가지의 경우의 수가 생겼다.
정적 페이지 방식의 초기 랜더링의 속도가 얼마나 빠른지 알게 되어 이미지들을 다운로드 하는 방향으로 진행하기로 했다.
처음엔 빌드시 public 폴더에 이미지들을 저장시켜 해당 이미지를 public 폴더에서 조회 하는 방법을 생각했다.
// utill/notions
if (block.type === 'image') {
if (block.image.type === 'file') {
if (!fs.existsSync(`/images/${block.id}.jpg`)) {
await fetch(block.image.file.url)
.then((res) => res.arrayBuffer())
.then((res) => {
fs.writeFileSync(`/images/${block.id}.jpg`, Buffer.from(res));
});
}
}
return {
id: block.id,
type: 'image',
caption: block.image.caption,
...(block.image.type === 'file'
? { url: `/images/${block.id}.jpg` }
: {}),
};
}
fs 모듈로 public 폴더에 동일 파일이 존재하는지 찾아서 writeFileSync 매서드를 통해 새로 생성하거나 저장되어 있는 파일을 조회하는 로직을 만들었다.
문제는 빌드 시 public 폴더에 파일 조회가 안 되는 에러가 발생했다.
- info Creating an optimized production build ..Error: ENOENT: no such file or directory, open '/images/4f4dbf92-96e0-4dca-9081-801655ef52e0.jpg'
at Object.openSync (node:fs:601:3)
at Object.writeFileSync (node:fs:2249:35)
at /Users/kimwoohyun/private/effective-memory/my-app/.next/server/chunks/150.js:119:43
at async convertBlock (/Users/kimwoohyun/private/effective-memory/my-app/.next/server/chunks/150.js:118:17)
at async /Users/kimwoohyun/private/effective-memory/my-app/.next/server/chunks/150.js:339:27
at async Promise.all (index 10)
at async rc (/Users/kimwoohyun/private/effective-memory/my-app/.next/server/chunks/150.js:338:35)
at async getNotionPageDetail (/Users/kimwoohyun/private/effective-memory/my-app/.next/server/chunks/150.js:349:26)
at async Page (/Users/kimwoohyun/private/effective-memory/my-app/.next/server/app/(main)/blog/[id]/page.js:1001:18) {
errno: -2,
syscall: 'open',
code: 'ENOENT',
path: '/images/4f4dbf92-96e0-4dca-9081-801655ef52e0.jpg'
}
• Only assets that are in the public directory at build time will be served by Next.js. Files added at runtime won't be available. We recommend using a third-party service like AWS S3 for persistent file storage. - Optimizing: Static Assets in Next.js
처음부터 정적인 파일을 public 폴더에 저장하는게 아닌 빌드 시 런타임에 새로 생성하여 저장하는 방법은 불가능하였기에 AWS의 S3 스토리지를 사용하여 저장하기로 했다.
// utill/notions
import AWS from 'aws-sdk';
...
if (block.type === 'image') {
const s3 = new AWS.S3({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY_ID as string,
},
});
let imageUrl;
if (block.image.type === 'file') {
const response = await fetch(block.image.file.url);
const imageBuffer = await response.arrayBuffer();
const contentType = response.headers.get('content-type');
const imageKey = `images/${block.id}.${getImageExtension(
contentType as string,
)}`;
const checkParams = {
Bucket: 'woo1031bucket',
Key: imageKey,
};
let isExist;
try {
await s3.headObject(checkParams).promise();
imageUrl = `https://${s3.config.endpoint}/${checkParams.Bucket}/${checkParams.Key}`;
isExist = true;
} catch (error: any) {
if (error.name === 'NotFound') {
isExist = false;
} else {
console.log('error:::', error);
throw error;
}
}
if (!isExist) {
const params = {
Bucket: 'woo1031bucket',
Key: imageKey,
Body: Buffer.from(imageBuffer),
};
const uploadResult = await s3.upload(params).promise();
imageUrl = uploadResult.Location;
}
}
return {
id: block.id,
type: 'image',
caption: block.image.caption,
...(block.image.type === 'file' ? { url: imageUrl } : {}),
};
}
...
파일을 읽어 생성한 URL이 내 버킷에 존재하는 지 확인 후 S3에 업로드 하거나 존재하면 해당 URL을 사용할 수 있게 진행을 했으며 결과도 상당히 만족스러웠다.

위 에러에 대한 issues들은 많았지만 참고 할 만한 자료가 마땅히 없어서 당황했다.
아직 notion에 업로드하는 공부 양에 비해 블로그 업로드 양이 적어 상관이 없지만 길게 봤을 때 서버에 정적 자원이 계속 쌓이는 건 문제가 있을 수도 있으며 AWS에 이미지 관련해 의존하고 있다는 사실이 찝찝하다.
추후 Notion 구조 트리를 생성하는 로직을 리팩토링하여 SSR으로 전환 할 수 있게 해야겠다.