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 측정 결과

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