원래는 블로그 포스트를 마크다운(.md) 파일로 레포지토리에 통으로 업로드 하다가, 장치에 구애받지 않고 포스트 CRUD를 할 수 있었으면 좋겠다는 생각에 저장 방식을 고민하게 됐다. 물론 아이폰이나 아이패드로도 마크다운 문서를 작성하거나 github 레포지토리에 업로드 하는 것이 어려운 건 아니지만, 좀더 확장성 있고 사용자(나ㅎ) 친화적인 퍼블리싱을 적용하고 싶었다.
블로그 포스트 저장하려고 어드민을 만들어서 에디터 붙이고 DB 연동하기는 너무 과하고, 적당히 가볍고 유연한 사용성을 가진 저장 방식은 없을까 생각하다가 Notion API가 떠올랐다.
Notion API에서는 Notion의 데이터베이스에 접근할 수 있는 엔드포인트를 제공한다. 접근하고 싶은 특정 Notion 데이터베이스를 정해놓고 그 안에서 문서를 생성하거나 수정/삭제하면, API의 response에 그 결과값이 반영된다. Notion을 블로그 포스트 DB로 사용하는 것은 아래와 같은 장단점이 있다.
👍 Good Parts
작성과 저장이 동기화 된다
Notion API를 연동하는 것은 사실 다른 DB를 연동하는 것과 별반 다르지 않다. 환경변수에 API key를 추가해서 원하는 DB에 접근한 뒤 key:value 형식으로 데이터를 불러올 수 있다.
하지만 다른 DB는 create, update, delete 등의 mutation 요청도 API 호출을 통해 이루어지는 반면, Notion API는 해당 문서에 직접 접근해서 수정할 수 있다는 장점이 있다(물론 API 호출을 통해서도 할 수 있다). 즉, 다른 DB 서비스는
마크다운 문서 작성 → create API 호출하거나 DB에 마크다운 문서를 직접 업로드
이런 단계를 거쳐야 한다면, Notion API의 경우 마크다운 문서 자체를 Notion 웹 또는 앱을 통해 다이렉트로 작성할 수 있으므로
마크다운 문서 작성( === 마크다운 문서 업로드 === DB에 해당 문서 저장)
따로 저장을 위한 작업 없이도 유저의 마크다운 작성이 곧 저장을 의미하는 행동이 되어 단계가 훨씬 축약된다.
블로그 포스트와 유사한 리스트-상세페이지 형태로 관리할 수 있다
Notion에 표 형태의 데이터베이스를 만들면, 보편적인 블로그 포스트의 리스트-상세페이지와 비슷하게 행마다 페이지를 생성해 디테일을 작성할 수 있다. 리스트로 보여주고 싶은 frontmatter(해당 포스트에 대한 제목, 설명, 작성일, 썸네일, 태그 등 요약정보)은 표 데이터베이스에 작성하고, 블로그 포스트 자체는 해당 행의 페이지로 따로 작성해주면 간편하게 리스트와 상세페이지를 분리해서 API 요청을 보낼 수 있다.
// Notion API를 통해 포스트 리스트를 호출하기
const fetchPosts = async () => {
return await (
await fetch(
`https://api.notion.com/v1/databases/${process.env.NEXT_PUBLIC_NOTION_DATABASE_ID}/query`,
{
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_NOTION_API_KEY}`,
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify({
sorts: [
{
property: "date",
direction: "descending",
},
],
}),
}
)
).json();
};
export default fetchPosts;
// Notion API를 통해 포스트 마크다운 영역을 호출하기
const markdown = await (
await fetch(`https://api.notion.com/v1/blocks/${id}/children`, {
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_NOTION_API_KEY}`,
"Notion-Version": "2021-05-13",
},
method: "GET",
})
)
.json()
.then((res) => res.results[0].code.text[0].plain_text);
이미지 호스팅을 알아서 해준다
원래는 썸네일이나 포스트 헤더 이미지를 public 디렉토리에 넣어주었는데, Notion DB 타입에는 이미지도 있으므로 표 데이터베이스에 직접 이미지를 업로드하고 response로 반환되는 호스팅 경로를 이미지 소스로 사용하면 된다.
👎 Bad Parts
serverSide에서만 요청할 수 있다
Notion API는 정책상 다른 DB들과 비슷하게 클라이언트에서는 호출할 수 없다. 그래서 원래 getServerSideProps
를 통해 Next.js 서버에서 호출하다가, 현재는 getStaticProps
로 아예 빌드 타임에 포스트 페이지를 전부 생성해서 배포하고 있다. 물론 getServerSideProps
로 요청하면 최신 데이터를 보장할 수 있겠지만, 쾌적한 렌더링을 위해 포스트가 추가될 때마다 배포를 트리거해 포스트 리스트를 업데이트 하는 방향으로 틀었다.
클라이언트에서 호출할 수 없다고 생각하면, 무한 스크롤 등 유저 행동에 따른 API 호출이 불가능하다고 생각하기 쉬운데, Next.js를 사용한다면 그냥 Next.js server에 라우터를 만들어 Notion API를 한 번 감싸주면 그만이다. 클라이언트에서 호출할 때는 Next.js API를 사용하고… 그렇기 때문에 serverSide에서만 요청할 수 있는 부분은 어떻게 보면 API key 보안상 당연한 것이고, 번거롭긴 하지만 큰 장애물은 아니라고 생각된다.
이미지 호스팅의 권한 기간이 짧다
Notion database에 업로드한 이미지는 일정 기간이 지나면 권한이 만료되어 더이상 정상 응답을 반환하지 않게 된다. 처음에는 이 사실을 모르고 갑자기 엑박 투성이가 된 블로그를 보고 당황했는데, 다시 배포하니 새로운 CDN 경로를 받아와 정상적으로 보이게 됐다.
시간이 지나면 권한이 만료되는 현상은 빌드 시에 딱 한 번만 API 요청을 보내는 getStaticProps
에서만 발생하는 것이니, 애초에 API 요청을 gSSP로 했으면 발생하지 않을 것이고, ISR을 통해 주기적으로 다시 API 요청을 보내도 발생하지 않을 듯 하다.
서치해보니 다른 개발자들은 빌드 당시 반환된 이미지를 저장해서 다른 안전한(권한이 만료되지 않는) 곳에 업로드 한 뒤 경로를 새로 만들기도 하는데, 그렇게까지 하면 굳이 Notion API를 사용하는 메리트를 잃어버리는 것 같아 ISR로 천천히 리팩토링 해볼 생각이다. 이미지가 만료되기 전에 부지런히 포스트를 작성하는 것도 하나의 방법이 될 수 있다;