Youngjae

Next.js + Notion API로 블로그 만들기

Next.js + Notion API로 블로그 만들기

2022.09.24

dev
nextjs
blog
 
개발자라면 자고로 ‘기술 블로그 하나쯤은 있어야지’하는 생각을 할 것 같다. 나 역시 그랬기에. 블로그를 운영하며 나의 생각과 배운 것들을 글로 잘 다듬어서 공유하고 싶었다.
하지만 글을 쓰는 것은 결코 쉬운 일이 아니다. 어쩌면 개발을 하는 것 만큼 공수가 들어갈 수 있는 일이다. 글을 쓰는 것도 어려운데 글을 올리기 위한 블로그까지 만들어야 한다면 글을 더욱 쓰기 싫어진다. 그런데 블로그에 포스팅을 하는 목적을 생각해보면 이 행동의 흐름은 어딘가 잘못됐다. 다른 사람들에게 보여주기 위한 글을 쓰고 싶은데 글을 보여줄 페이지 제작에 집중하다보니 거기에 힘을 많이 쏟은 나머지 글을 쓰지 못한다라.. 더 중요한건 사실 껍데기가 아닌 내용이다. 프론트엔드 개발자로써 더 예쁘고 아름다운 인터페이스를 제공하는 궁극의 블로그를 만들고 싶은 욕심이 앞선다. 내용을 더 잘 전달하기 위해 컴포넌트를 통일성있게, 재사용가능하게 만들어 잘 분리하고, 깔끔한 유저 인터페이스를 통해 나의 글을 더 돋보이고 읽고 싶게 만들고 싶다. 하지만 무엇보다 블로그에 유익한 내용이 있어야 글을 읽으려 올거란 생각을 머리속에 갖고 있자.
그래서 나는 작년까지 Medium을 이용했다. Medium에서 글을 쓸 때에는 에디터에서 한글이 씹히거나 오류가 나는 불편이 있기도 하고, 초고를 쓴 계속 다시 읽어보며 이상한 부분을 수정하는 편이기 때문에 노션에 블로그 글을 먼저 써놓고 완성이 되면 복사해서 올렸다. 그리고 글을 몇 편 써보니 미디엄에서 찾을 수 있는 아쉬운 점(카테고리의 부재, 내게 좀 큰 폰트) 등이 눈에 밟혀 직접 블로그를 만들기 위한 시도는 했었다.
 

Next.js + Notion 조합을 선택한 이유는?

처음에는 대표적인 정적블로그 프레임워크였던 Jekyll을 사용해 Github 블로그(youngjae99.github.io)를 사용하다가 리액트 기반의 프레임워크로 블로그를 전환하고 싶다는 하는 생각을 했다. Ruby 기반의 Jekyll보다는 React기반의 Gatsby를 사용하는 것이 아무래도 리액트에 친숙한 내가 컴포넌트를 개발하기도 편하고, 참고할 수 있는 자료들과 플러그인이 풍부할 것이기 때문이다. 그리고 Gatsby의 gatsby-starter-blog template를 이용해서 새 블로그를 만들기 전 이것저것 간단히 테스트해보며 만들어보기도 했다.
하지만 Gatsby로 간단히 테스트해보면서, Gatby가 아닌 Next.js를 선택하기로 했다. 그 이유는 아래와 같다.
  1. Notion API를 사용하기 위해서는 async, fetch 중심으로 이루어지므로 GraphQL을 도입할 필요가 없었다. Gatsby는 GraphQL만을 통해 컨텐츠 데이터를 가져오는 작업에 맞춰져 있다.
  1. Next.js는 페이지 단위로 SSR, SSG, CSR을 쉽게 선택할 수 있는 프레임워크이기 때문에 추후 다양한 인터랙션이 필요한 페이지(수시로 데이터가 변경되거나 앱 내 캔버스나 웹 뷰와 같은 CSR이 필요한 경우)를 추가할 수 있는 이점이 있을 것이다.
  1. 블로그로써의 기능 뿐 아니라 나의 포트폴리오 사이트로써의 기능도 할 수 있었으면 했는데, Next.js로 만들고 있던 포트폴리오 페이지 레포에 blog 페이지를 만들어서 개발에 들어갈 수 있다.
  1. Gatsby가 Next.js보다 SSG가 빠르다고는 하지만 위와 같은 Next.js의 장점들을 상쇄할 정도로 차이가 유의미하게 크지 않았다.
즉, 정적인 페이지에 특화되어있는 인상의 Gatsby보다는 CSR, SSR, SSG 등을 유연하게 넘나들며 여러 기능들을 시도하고 추가하기 용이한 Next.js가 더 적합하다고 생각했다.
 
프레임워크로 Next.js를 선택했다면, 이제 CMS를 고민해야할 때였다. .md 확장자를 가진 마크다운 파일로 저장된 글을 레포에 함께 저장하고 싶지 않았다. 글을 배포하려면 GitHub에 push를 해야하기도 하는 것이 불편했다. Strapi, Dato CMS 등의 Headless CMS를 고민했다. 그런데 위에서 말했듯 나는 항상 블로그에 글을 쓰기 전에 초안을 notion에 기록하기 때문에, 굳이 다른 CMS를 도입하지 말고 Notion을 활용해 원클릭으로 블로그를 만들어 글을 publish(공개)할 수 있도록 한다면 너무 편할 것 같았다. 글을 쓰기 편해야 글을 잘 쓸 수 있지 않을까.
그러던 중 Notion API가 작년 5월 베타 버전으로 공개된 후 됐음을 알았다.
만들고 싶으면 만들어야지. 그래서 바로 시작했다.
 

블로그를 위한 기능들

블로그를 개발하기 앞서 일정 산정, 브랜치 구분 등을 위해 필요한 기능들을 간단하게 리스트업 해보았다.
우선순위를 산정할 때에는 블로그로 써의 최소한의 필수적인 기능을 P1으로, 그 밖에 있으면 좋을 법한 기능들을 P2, P3 순으로 적었다.
 
  • [P1] Next SSG ✅
  • [P1] Notion API를 통해 글을 쉽게 publish, 공개/비공개 기능 ✅
  • [P1] 포트스별 SEO: og-tag, sitemap.xml, robots.txt 자동 생성 ✅
  • [P1] 포스트 태그(카테고리) 기능 ✅
  • [P1] 글 목차 자동 생성 (ToC) ✅
  • [P1] 코드 하이라이팅 ✅
  • [P2] Google Analytics 지원 - 방문자 트래킹 ✅
  • [P2] 포스트 검색 기능 ✅
  • [P2] 포트스 별 comment 기능 ✅
  • [P2] 모바일 최적화 (media query) ✅
  • [P3] 다크모드 ✅
  • [P3] 포스트 like 기능
  • [P3] prev, next 포스트 버튼 / 관련 포스트 추천 기능
(✅ 는 2022.11.30. 기준 완료된 기능)
 
를 많이 참고했다.
 
그리고 이 블로그를 개발하는 과정에서 원칙이 하나 있다 최대한 외부 라이브러리나 템플릿을 쓰지 않는 것이고, 필요한 기능이 있다면 되도록 직접 구현하는 것이었다.
 

1. Notion API

처음에는 Cloudflare의 Worker을 이용해서 개발했다. 다시 생각해보니 Next.js에서 API를 제공하는데 굳이 Cloudflare Worker을 사용할 필요가 없었다. 때문에 블로그를 업데이트 하면서 /pages/api 에 blog와 관련된 api를 올려두기로 했다.
notion image
/pages/api 아래에 about(about 페이지에서 보여줄 정보 fetch), posts(블로그 포스트 리스트, 포스트 내용 fetch)로 나누어서 개발했다.
Notion을 DB를 만들어야한다. 나는 그동안 초안을 쓰는 용도로 쓰던 DB가 있어 이를 Notion API에 연결했다.
예전부터 노션에 다루고 싶은 글감이 생각나면 DB에 제목과 간략한 개요를 적어놨는데 글의 완성도와 시간을 핑계로 세상의 빛을 보지 못한 글이 많다.. 이걸 쉽게 publish하도록 하는 것이 이 Notion+Next.JS블로그의 목적.
예전부터 노션에 다루고 싶은 글감이 생각나면 DB에 제목과 간략한 개요를 적어놨는데 글의 완성도와 시간을 핑계로 세상의 빛을 보지 못한 글이 많다.. 이걸 쉽게 publish하도록 하는 것이 이 Notion+Next.JS블로그의 목적.
 
 

2. Next.js로 Notion 파싱하기

  • react-notion-x 라이브러리를 사용하기로 했다.
같은 마크다운이라고 해도 노션, 깃헙에서 지원하는 기능들과 보는 것이 다 다른데, md 파일로 /contents 폴더에 export해서 올리고, 이를 또 파싱하는 과정에서 기대한 것과 다르게 보이는 경우가 있었다. 티스토리나 미디엄은 온라인에서 글을 편집하는 훌륭한 기능도 있었기 때문에 편한 대신 커스텀에 제약이 많고, 개츠비로 마크다운 블로그를 할 경우 너무 보이는 것에 문제가 많았다. 나는 노션에서 에디팅하기 때문에 신경쓰지 않고 포스트에 오로지 집중할 수 있었다. 이것이 바로 WISWIG…
 

3. UI 개발

Velog나 Tistory, Medium과 같은 블로그 서비스를 사용하지 않고, 심지어 Gatsby를 사용하지 않은 것도 아무것도 없는 상태부터 스스로 블로그의 모든 기능을 개발하고자 했기 때문이다.

a. Post card list

 
notion image
 
제목작성시간, 썸네일 정도의 간결한 정보를 주는 디자인을 유지하도록 했다.
정말 간단한 컴포넌트지만 Toss Feed, Design Compass 등의 기술/디자인 블로그를 참고하면서 이 컴포넌트에 얼마나 많은 정보를담으면 좋을지, 썸네일 이미지 크기는 어디에(왼쪽/오른쪽) 배치하는 것이 좋을지 디테일 적인 부분에 있어 많이 생각했다.
 
Card에 들어갈 수 있는 아이템과 그에 따른 배치를 고려해서 컴포넌트 디자인을 생각했다. 어떤 레이아웃이 가장 효과적으로 나의 블로그 포스트를 전달할 수 있을까?
Card에 들어갈 수 있는 아이템과 그에 따른 배치를 고려해서 컴포넌트 디자인을 생각했다. 어떤 레이아웃이 가장 효과적으로 나의 블로그 포스트를 전달할 수 있을까?
현재 PostCardList 컴포넌트는 아주 심플하게 디자인한 편이라 보여줄 정보의 양을 카테고리(tag), 내용(description) 등을 유연하게 변경할 수 있도록 재설계하려고 한다. 그리고 위에 토글 버튼을 통해 List View와 Card View를 선택할 수 있도록 할 계획이다.
 

b. Post tags

블로그 글을 쓸 때 아쉬운 점이 나만의 글 태그를 달기가 쉽지 않다는 것이다.
카테고리별 글을 잘 볼 수 있도록 notion에 multi-tag를 이용해 notion에 정리하곤 했다.
notion image
 
 
이제 이렇게 저장한 tag들을 1. post card에 보여주고, 2. tag별로 filtering하거나 모아서 보여줄 수 있는 기능이 필요하다.
 
1차적으로 만들어진 블로그
1차적으로 만들어진 블로그

c. Post page

react-notion-x를 잘 커스텀해서 블로그에 맞도록 하면 된다.
스타일 수정은 Style Wrapper 컴포넌트를 만들어 해당하는 컴포넌트의 클래스명에 해당하는 속성들을 바꿔주었다.
 

4. 블로그 기능 개발

Giscus를 이용한 댓글과 좋아요

jekyll을 이용해 만든 블로그에서는 Disqus를 이용했다. 소셜 로그인을 지원해서 블로그에 방문할 (비개발자, Github 계정이 없는) 다양한 사람들이 댓글을 달 수 있다. 그러나 Disqus는 몇 가지 치명적인 단점이 있는데, 보기 싫은 광고가 있고, . GitHub의 신기능인 Discussion 기능을 이용한 Giscus (아마 Discus + Github이라..)을 사용하기로 했다. utterence가 더 유명하고 구글링 했을 때 예제가 많았지만 Giscus가 Utterence보다 나은 이유를 알게 되었고, 더는 굳이 utterence를 선택할 이유가 없다고 생각했다.
아래처럼 몇가지 옵션들과 함께 Giscus 컴포넌트를 만들어주면 끝이다!
import Giscus from "@giscus/react"; ... <Giscus id="comments" repo="youngjae99/youngjae-blog-comments" repoId="~~~~~~~~" category="General" categoryId="~~~~~~~~~" mapping="title" reactionsEnabled="1" emitMetadata="0" inputPosition="top" theme={theme === "light" ? "light_tritanopia" : "dark_tritanopia"} lang="en" loading="lazy" />
Giscus를 이용한 댓글 작성 기능 추가 (2022.11.2)
Giscus를 이용한 댓글 작성 기능 추가 (2022.11.2)
 

SSG + sitemaps 설정

shared to web으로 해야 notion client로 가져올 수 있다.
shared to web으로 해야 notion client로 가져올 수 있다.
빌드를 했을 때 /pages/posts/{post_id} 페이지들이 빌드가 안되는 오류가 있었다.
Error occurred prerendering page "/posts/1e8e2b5f-d89a-407b-8164-2fecf31ad07c". Read more: https://nextjs.org/docs/messages/prerender-error Error: Notion page not found "1e8e2b5fd89a407b81642fecf31ad07c" at re.getPage (file:///Users/aaron/dev/next-projects/youngjae_profile/node_modules/notion-client/build/index.js:1:1083) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async getStaticProps (/Users/aaron/dev/next-projects/youngjae_profile/.next/server/pages/posts/[postId].js:144:21) .... > Build error occurred Error: Export encountered errors on following paths: /posts/[postId]: /posts/0b9e9d22-1b51-4c8d-90a0-54e4b49ab9a3 /posts/[postId]: /posts/0c499705-f84c-425c-af02-13a9df0a222d /posts/[postId]: /posts/0da670df-8ffa-485b-b417-7ab048792708 /posts/[postId]: /posts/0edf423f-f01e-4aee-bb46-ec2eb500d36b /posts/[postId]: /posts/0f6c66cf-4f8c-4eec-84b7-b43f6ce04c07 /posts/[postId]: /posts/123caf9a-2adb-4204-a4eb-2c375851d671 /posts/[postId]: /posts/12c03f10-6344-4144-92ae-1c83ccb9d0db /posts/[postId]: /posts/1617dfc1-b16e-49b0-a545-74fa702c93c6 /posts/[postId]: /posts/16288cc8-7ee2-41eb-a019-d6a81be20735 .... error Command failed with exit code 1.
visibility 속성을 true(=checked)로 했어도 간혹 실수로 내가 Share 노션 페이지 공개가 안되었을 시 에러 핸들링도 해주자.
/pages/blog.tsx
export interface Post { id: string; properties: any; [key: string]: any; } interface Posts { posts: Post[]; } const Blog: React.FC<Posts> = ({ posts }: Posts) => { return ( <Layout> {posts ? posts.map((post: Post) => <PostCard key={post.id} post={post} />) : null} </Layout> ); }; export const getStaticProps = async () => { const posts = await getNotionPosts(); return { props: { posts, }, revalidate: 10, }; }; export default Blog;
/pages/posts/[postId].tsx
const Post: React.FC<{ pageData: any; recordMap: any }> = ({ pageData, recordMap, }: { pageData: any; recordMap: ExtendedRecordMap; }) => { const metadata = generateBlogPostMeta(pageData); const createdTime = pageData?.created_time; const tags = pageData?.properties.Tags.multi_select.map((tag) => tag.name); console.log(tags); if (!pageData) { // pageData가 없는 경우 HiddenPost임을 보여주기 return ( <Layout> <HiddenPost /> </Layout> ); } return ( <Layout> <NextSeo title={metadata.title} description={metadata.description} openGraph={metadata.openGraph} /> <NotionPage recordMap={recordMap} rootPageId={rootNotionPageId} postTime={createdTime} tags={tags} /> </Layout> ); }; export const getStaticPaths: GetStaticPaths = async () => { const posts = await getNotionPosts(); const paths = posts.map((post) => ({ params: { postId: post.id }, })); return { paths, fallback: false, }; }; export const getStaticProps = async ({ params }) => { const pageId = params.postId as string; try { const recordMap = await notion.getPage(pageId); const pageData = await getNotionWithPageId(pageId); return { props: { pageData, recordMap, }, revalidate: 10, }; } catch { // notion에서 page share가 안되어있어 public access가 불가능한 경우 return { props: { pageData: null, recordMap: null, }, revalidate: 10, }; } }; export default Post;
 

Sitemaps

구글링할 수 있도록 하기 위해서는 next-sitemap 라이브러리를 사용했다. 몇가지 설정만 해주면 자동으로 robots.txt와 sitemap.xml 파일을 생성해주고, post의 경우 SSG를 사용하기 때문에 dynamic/server-side sitemap 관련 설정 없이도 사이트맵이 잘 생성된 것을 확인할 수 있다.
notion image
notion image
 
Google Search Console에 등록을 하고 잘 잡히는지 확인하면 된다.
그런데 잘 안잡혔다. 분명 sitemap 제출을 했는데 왜 안잡히지? 당황했다.
몇가지 가설이 있었다.
  1. sitemap 제출 후 google crawler가 돌기까지 시간이 걸린다.
 
 

Open Graph Tag(OG-tag)

페이스북 개발 그룹이나 트위터, 카카오톡을 통해 나의 글이 많은 사람들에게 공유되는 것만큼 글 쓰는 보람이 되는 일이 없을 것이다. 구글 검색에 있어서 상단에 노출되는 것에도 영향을 주기 때문에 중요하다. 이 때 공유되는 이미지와 텍스트도 잘 관리할 필요가 있다는 것을 알았다. 예전에 NFT 프로젝트의 이벤트에서 트위터 공유용 카드 이미지 페이지를 만들 때 알게된 OG-tag를 이용하면 되는 것을 알았다. 다만 그 때는 리액트로 동적으로 생성해야했기 때문에 추가적인 서버가 필요했지만, 이번에는 next를 사용했기 때문에 next-seo 라이브러리를 이용해서 OG tag를 편하게 생성할 수 있었고, 잘 잡히는 것을 확인할 수 있었다.
notion image
 
 

맺으며

Next.js + Notion의 조합으로 블로그를 일주일동안 짬짬이 시간을 내어 완성하니 뿌듯하고 만족스럽다. 그동안 블로그를 처음부터 내 손으로 직접 만들고 싶었지만 JAM stack과 같은 기술스택을 공부하고 선택에 있어 비교하느라 블로깅 자체를 미루게 되는 나의 모습을 더는 볼 수 없었고, 블로그는 글을 공유하기 위한 수단일 뿐 주객이 전도되면 안된다고 생각을 했다.
특히 Notion API를 이용해 노션을 CMS 툴로써 사용하는 레퍼런스가 없는 것 같아 직접 API를 만들어보고, 기능을 덧붙이면서 재미를 느꼈다. 노션을 통해 글을 한번 publish해보니 글을 쓰고 publish하는데까지의 간단한 사용자 경험에 만족감이 꽤 크다. 앞으로 계속 블로그에 다양한 기능을 추가하고 시도를 하면서 새로운 기술을 가장 먼저 적용해보면서 배우는 곳으로 사용해야겠다.
무엇보다 글 쓰는 좋은 습관을 가지는 것이 우선이다.
이제 남은건 꾸준하고 배우고, 성장하고 열심히 글로 기록해보기.
 
공유하기

Youngjae Jang

Copyright © 2022, All right reserved.