Notion API로 만드는 나만의 기술 블로그 🚀


들어가며 - 왜 블로그를 만들게 되었나요? 🤔

개발자로서 학습 내용을 기록하고 공유하는 것은 매우 중요한 일입니다. 저는 그동안 Notion에 개발 학습 내용을 기록해왔는데, 이를 블로그 형태로 공유하고 싶다는 생각이 들었습니다. 그러나 velog나 tistory 등 블로그 플랫폼으로 마이그레이션하기에는 많은 시간이 필요했습니다. 노션의 이미지는 aws s3로 저장되는데, 이 데이터를 옮기는 작업이 번거로웠기 때문입니다. 그리고 가장 큰 이유로, 익숙한 Notion으로 계속 글을 작성하고 싶었습니다. 그래서 Notion API를 활용해 직접 블로그를 구축하기로 결정했습니다.

기존 Notion 학습 자료들
기존 Notion 학습 자료들

Notion API로 블로그 만들기 ⚡️

Next.js + Notion API

블로그 구축을 위해 Next.js를 선택했습니다. SSG(Static Site Generation)를 활용하면 빌드 타임에 Notion의 컨텐츠를 가져와서 정적 페이지를 생성할 수 있기 때문입니다.

export const notion = new Client({
auth: process.env.NOTION_API_KEY,
});
export async function getPage(pageId: string) {
const page = await notion.pages.retrieve({ page_id: pageId });
const blocks = await notion.blocks.children.list({ block_id: pageId });
return {
page,
blocks: blocks.results,
};
}

Notion API를 사용하기 위해서는 인증키가 필요합니다. 인증키는 환경변수로 관리해주었습니다. 위 코드는 페이지 ID를 받아 해당 페이지의 정보와 하위 블록들을 가져오는 함수입니다.

Notion Block 렌더링 구현

가장 핵심적인 부분은 Notion의 각 블록을 React 컴포넌트로 변환하는 작업이었습니다. 제목, 단락, 코드 블록, 이미지 등 Notion의 다양한 블록 타입을 처리하기 위해 다음과 같은 컴포넌트를 구현했습니다.

export default function Block({ block }: { block: BlockObjectResponse }) {
switch (block.type) {
case 'paragraph':
return <p>...</p>;
case 'heading_1':
return <h1>...</h1>;
case 'code':
return <Highlight theme={themes.github}>...</Highlight>;
// ... 기타 블록 타입들
}
}

Notion API는 각 블록의 타입에 따라 다른 데이터 구조를 반환합니다. 이를 적절한 React 컴포넌트로 변환하는 것이 핵심 작업이었습니다. 특히 코드 블록의 경우 prism-react-renderer를 사용하여 구문 강조 기능을 구현했습니다.

SSG를 활용한 빌드 타임 데이터 페칭

Next.js의 SSG를 활용해 빌드 타임에 모든 페이지를 생성하도록 구현했습니다.

export const dynamic = 'force-static';
export async function generateStaticParams() {
const postsPages = await getAllPosts(PAGE_ROUTES.POSTS.id);
const notesPages = await getAllPosts(PAGE_ROUTES.NOTES.id);
return [...postsPages, ...notesPages].map((page) => ({
slug: page.id,
}));
}

이렇게 generateStaticParams를 설정해주고 force-static 으로 설정해주면, 해당 페이지들을 빌드타임에 페칭하여 준비합니다. 빌드 로그에는 아래와 같이 나오게 됩니다.

Vercel의 빌드 로그
Vercel의 빌드 로그

이 방법의 단점으로는 빌드에 시간이 더 많이 소요된다는 것과, 글을 작성하고 다시 한번 빌드를 돌려주어야 한다는 점이 있습니다. 그러나 빌드에 시간은 5분이면 충분하고, 이 마저도 Vercel에 레포를 연동하면 main에 push 시에 CI/CD가 되기 때문에 긴 시간은 아니었습니다. 또한 글을 작성하는 도중에는 보여주고 싶지 않았고, 글 작성 후 작성 완료를 누르듯이 재배포를 돌려주면 새로운 게시글이 업로드 된다는 점에서 오히려 좋았습니다.

이미지 최적화 📸

Notion의 이미지 URL은 일정 시간이 지나면 만료되는 문제가 있었습니다. 이를 해결하기 위해 빌드 시점에 모든 이미지를 다운로드하고 최적화하는 방식을 구현했습니다. sharp 라이브러리를 사용하면 이미지를 webp로 변환하여 저장하면서 용량을 최적화할 수 있었습니다.

sharp 라이브러리를 통한 webp 최적화 전/후 용량 차이
sharp 라이브러리를 통한 webp 최적화 전/후 용량 차이
async function downloadImage(imageUrl: string, fileName: string) {
const response = await axios.get(imageUrl, { responseType: 'arraybuffer' });
await sharp(Buffer.from(response.data))
.webp({ quality: 80 })
.toFile(filePath);
}

해당 함수를 통해 빌드 타임에 모든 이미지를 다운로드하면서, WebP 포맷으로 최적화할 수 있었습니다. 이미지 사이즈는 5~10%로 줄어들었고, 빌드 타임 이후 이미지 URL이 만료되는 문제를 비용 없이 해결했습니다. 해당 스크립트를 빌드 전에 실행해주기 위해 pakage.json 도 수정해주었습니다.

"build": "npm run download-images && next build",

사실 모든 페이지의 이미지를 새로 불러오는 것은 아닙니다. 이는 Notion API가 한번에 불러올 수 있는 최대 블록이 100개이기 때문인데요, 제 블로그에는 100개 블록이 넘는 페이지들이 꽤 있었기 때문에 이러한 페이지 같은 경우 무한스크롤을 이용해서 다시 블록들을 불러오도록 했습니다.

useEffect(() => {
const observer = new IntersectionObserver(
async (entries) => {
if (entries[0].isIntersecting && hasMore && !isLoading) {
setIsLoading(true);
try {
const data = await fetchBlocks(pageId, cursor || '');
setBlocks((prev) => [...prev, ...data.blocks]);
setCursor(data.next_cursor);
setHasMore(data.has_more);
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
}
},
{ threshold: 1.0 },
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [cursor, hasMore, isLoading, pageId]);

여기에서 react-query와 같은 라이브러리를 사용하지 않은 이유는, Notion API를 클라이언트에서 호출하면 CORS 문제가 발생했기 때문입니다. 따라서 저 fetchBlocks 함수를 next의 use server 를 활용해서 서버에서 페칭하도록 만들었습니다.

'use server';
import { getPage } from './notion';
export async function fetchBlocks(pageId: string, cursor?: string) {
try {
const response = await getPage(pageId, cursor);
return response;
} catch (error) {
throw new Error('Failed to fetch blocks');
}
}

마치며

이 프로젝트를 통해 Notion API를 활용한 블로그 시스템을 성공적으로 구축할 수 있었습니다. 특히 이미지 최적화와 SSG를 통한 성능 개선은 사용자 경험을 크게 향상시켰습니다.

Lighthouse 측정 결과
Lighthouse 측정 결과

앞으로도 지속적인 개선을 통해 더 나은 블로그 경험, 좋은 인사이트를 제공하도록 하겠습니다.