1000PAGE 지식 저장소
Astro와 Cloudflare Pages로 만든 개인 AI 학습 지식 저장소 겸 포트폴리오 사이트
- Astro
- TypeScript
- Markdown
- Pagefind
- Cloudflare Pages
프로젝트 개요
AI·머신러닝 학습 내용이 ChatGPT 대화, 노션, PDF, 로컬 폴더에 흩어져 있어 다시 찾기 어려웠다. 이를 하나의 사이트로 모으고, 동시에 취업용 포트폴리오로도 쓸 수 있게 만든 정적 사이트다.
지금 보고 있는 이 사이트가 그 결과물이다.
문제 정의
학습 자료가 흩어지면 세 가지 문제가 생긴다.
- 다시 못 찾는다 — 어디에 적었는지 기억나지 않아 같은 내용을 반복해서 공부한다
- 설명할 수 없다 — 조각난 메모는 남에게 보여줄 수 없다
- 연결되지 않는다 — 개념과 실제 프로젝트가 따로 논다
단순한 블로그로는 3번이 해결되지 않는다. 문서끼리, 그리고 문서와 프로젝트가 서로 연결되는 구조가 필요했다.
주요 사용자
사이트 운영자 본인이 1차 사용자다. 2차 사용자는 채용 담당자와 면접관이다. 이 둘의 요구가 다르다.
| 사용자 | 필요한 것 |
|---|---|
| 본인 | 빠른 검색, 카테고리 탐색, 쉬운 글쓰기 |
| 채용 담당자 | 프로젝트 구조, 기술 선택 이유, 문제 해결 과정 |
요구사항
기능
- Markdown 파일을 넣으면 자동으로 페이지 생성
- 전체 문서 검색
- 카테고리·태그 기반 탐색
- 브라우저에서 글 작성(CMS)
- 다크모드, 모바일 대응
제약
- 운영 비용 0원
- 콘텐츠가 특정 서비스에 종속되지 않을 것
- 서버 관리를 하지 않을 것
기술 스택
기술 선택의 이유가 이 프로젝트의 핵심이다.
| 기술 | 용도 | 선택 이유 |
|---|---|---|
| Astro | 사이트 프레임워크 | 기본이 정적 HTML이라 JS가 거의 나가지 않는다. Content Collections가 프론트매터를 타입 검증해준다 |
| Markdown | 콘텐츠 | Git으로 버전관리되고 서비스 종속이 없다. 이전 비용이 사실상 0 |
| Pagefind | 검색 | 서버도 API 키도 필요 없다. 빌드 결과물을 훑어 인덱스를 만든다 |
| Cloudflare Pages | 배포 | 무료 대역폭 제한이 사실상 없고, 향후 Workers/D1로 백엔드를 붙이기 쉽다 |
| Shiki | 코드 하이라이트 | 빌드 타임에 처리되어 런타임 JS가 0 |
React를 쓰지 않았다. 화면에 상태 관리가 필요한 부분이 없어 프레임워크를 넣으면 JS 번들만 늘어난다.
시스템 아키텍처
flowchart TD
Admin[관리자] --> CMS[Git 기반 CMS]
CMS --> GH[GitHub Repository]
GH --> CF[Cloudflare Pages]
CF --> Build[Astro 빌드 + Pagefind 색인]
Build --> Site[정적 사이트]
Visitor[방문자] --> Site
서버가 없다. 글을 쓰면 GitHub에 커밋되고, 그것이 빌드를 트리거해 정적 파일이 배포된다.
데이터 흐름
Markdown 파일
→ Content Collections (zod 스키마 검증)
→ getStaticPaths()로 라우트 생성
→ 정적 HTML
→ Pagefind가 HTML을 훑어 검색 인덱스 생성
스키마 검증이 빌드 단계에 있어, 프론트매터가 잘못되면 배포 전에 잡힌다.
핵심 기능
- 자동 페이지 생성 —
src/content/에 md를 넣으면 목록과 상세가 자동 생성 - 카테고리 관리 — 카테고리가 코드가 아니라 파일이라 CMS에서 추가·순서변경 가능
- 정적 검색 — 서버 없이 전문 검색
- 다크모드 — FOUC 없이 즉시 전환
- 문서 연결 — 태그·카테고리 기반 자동 추천 + 수동 지정
주요 코드
카테고리를 코드가 아닌 데이터로 뺀 부분이 설계의 핵심이다. 처음에는 z.enum으로 카테고리를 고정했는데, 그러면 카테고리를 추가할 때마다 코드를 고쳐야 했다.
// 카테고리 1개 = 파일 1개. 파일을 추가하면 사이트에 자동 반영된다.
const categories = defineCollection({
loader: glob({ pattern: '*.{md,mdx}', base: './src/content/categories' }),
schema: z.object({
title: z.string(),
order: z.number().default(99), // 이 값으로 순서를 바꾼다
icon: z.string().optional(),
}),
});
대신 문서의 category는 z.enum을 쓸 수 없게 되어 오타 검증을 잃었다. 이를 보완하려고 빌드 시 경고를 내는 검증 함수를 넣었다.
실행 결과
지금 보고 있는 사이트 전체가 실행 결과다. 홈, 문서 목록, 카테고리, 태그, 검색이 모두 동작한다.
검증 방법
| 항목 | 방법 | 결과 |
|---|---|---|
| 빌드 | npm run build | 0 errors |
| 배포 | GitHub push → 자동 빌드 | 2~3분 내 반영 |
| 검색 | npm run preview 후 검색 | 한국어 검색 동작 |
| 접근성 | 키보드만으로 탐색 | 전체 도달 가능 |
발생한 문제
glob 로더에 exclude 옵션이 있다고 가정하고 카테고리 폴더를 제외했는데, 빌드가 실패했다.
InvalidContentEntryDataError: docs → categories/agents
category: Required
카테고리 파일이 문서 컬렉션으로 들어와 문서 스키마로 검증된 것이다.
해결 과정
- 오류 메시지의
docs → categories/agents를 보고 컬렉션이 겹쳤음을 파악 node_modules의glob.d.ts를 직접 열어 타입 정의를 확인GlobOptions에exclude가 아예 없다는 것을 확인.pattern,base,generateId,retainBody,deferRender만 존재pattern이 배열을 받는 것을 이용해 부정 패턴으로 교체
pattern: ['**/*.{md,mdx}', '!categories/**', '!projects/**']
문서를 추측하지 않고 설치된 타입 정의를 직접 확인한 것이 해결의 핵심이었다.
한계
- 카테고리를 CMS에서 관리하기 위해 문서
category의 스키마 검증을 느슨하게 했다. 오타가 빌드를 깨뜨리지 않고 경고로만 남는다 - 관련 문서 추천이 O(N²)이라 문서가 1,000개를 넘으면 빌드가 느려진다
- 한국어 검색에서 조사 처리가 안 된다. “임베딩을”과 “임베딩”의 결과가 다를 수 있다
- 모바일에서 사이드바가 서랍이 아니라 본문 아래로 내려간다
향후 개선
- 문서가 쌓이면 한국어 검색 품질을 실측하고, 부족하면 검색어 정규화를 추가
- 관련 문서 계산에 태그 역인덱스를 도입해 O(N)으로 개선
- AI 기반 자연어 검색(Cloudflare Workers + 벡터 DB)
GitHub
저장소는 비공개다. 초안이 커밋 히스토리에 남기 때문이다.
데모
이 사이트 자체가 데모다.
발표 자료
없음.