Day 1-2: 첫 삽질의 기록 - 옵시디언에서 웹사이트까지
김솔
엔터를 누르는 손이 떨렸다
2025년 11월 14일, 오전 9시 58분.
브라우저에 EasyNext 사이트가 열려 있다.
바이브 코딩으로 웹사이트를 뚝딱 만드는 영상들을 보고
"커서맛피아"라는 유튜브 채널을 좋아하게 되었다.
그 영상들 중 하나에서 EasyNext를 알게 됐다.
"Next.js 프로젝트를 가장 쉽게 시작하는 방법"이라는 문구가 눈에 들어왔다.
복잡한 설정 없이 클릭 몇 번으로 프로젝트를 만들 수 있다고 했다.
엔터를 누르는 손이 미세하게 떨렸다.
TypeScript, Tailwind CSS, App Router... 뭔지도 잘 모르지만 권장 설정 그대로 두었다.
화면에 뭔가가 쏟아져 내린다. 폴더가 생기고, 파일이 생기고, 의존성이 설치된다.
첫 커밋.
9fcc2f9 - Initial commit from Create Next App
이 한 줄이 206개의 커밋으로 이어질 줄은 몰랐다.
첫 번째 관문: 옵시디언 연동
"위키링크가 뭐야?"
가장 먼저 해결해야 할 문제는 옵시디언 마크다운을 웹에서 읽을 수 있게 변환하는 것이었다.
옵시디언에서는 이렇게 링크를 건다.
이 글은 [다른 글](/archive/%EB%8B%A4%EB%A5%B8%20%EA%B8%80)을 참고했습니다.
이미지는 처럼 넣습니다.
하지만 웹 브라우저는 이 문법을 모른다.
"Claude, 옵시디언의 위키링크를 일반 마크다운 링크로 바꿔줘."
// [표시 텍스트](/archive/%EB%A7%81%ED%81%AC) → [표시 텍스트](/archive/링크)
content = content.replace(
/\[\[([^\]|]+)\|([^\]]+)\]\]/g,
'[$2](/archive/$1)'
)
// [링크](/archive/%EB%A7%81%ED%81%AC) → [링크](/archive/링크)
content = content.replace(
/\[\[([^\]]+)\]\]/g,
'[$1](/archive/$1)'
)
정규표현식.
코딩 공부할 때 배웠던 것 같은데, 하나도 기억이 안 났다.
Claude가 설명해줬다. /\[\[/는 "[" 문자를 찾고, ([^\](/archive/%22%20%EB%AC%B8%EC%9E%90%EB%A5%BC%20%EC%B0%BE%EA%B3%A0%2C%20%60(%5B%5E%5C)+)는 "]"가 아닌 모든 문자를 캡처하고...
솔직히 반만 이해했다. 하지만 작동했다. 그게 중요했다.
Date 객체의 배신
글 목록을 불러오는 건 금방 됐다. 파일을 읽고, frontmatter를 파싱하고, 정렬하면 끝.
그런데 화면에 글을 뿌리려고 하니 에러가 터졌다.
Error: Date objects cannot be serialized to JSON
"뭔데 이게?"
알고 보니, gray-matter 라이브러리가 frontmatter의 date: 2025-11-14를 자동으로 JavaScript Date 객체로 변환하고 있었다. 그런데 React 컴포넌트에는 Date 객체를 그대로 전달할 수 없다.
"Claude, 이거 어떻게 해?"
date: typeof data.date === 'string'
? data.date
: data.date?.toISOString().split('T')[0] || ''
Date 객체면 문자열로 변환하고, 아니면 그대로 쓴다.
10줄 코드에 30분을 썼다.
삽질의 시작이었다.
두 번째 관문: 댓글 시스템
Giscus vs Supabase
블로그에 댓글 기능을 넣고 싶었다.
처음에는 Giscus를 고려했다.
GitHub Discussions 기반이라 설정이 간단하다고 했다.
하지만 문제가 있었다.
"GitHub 계정이 없으면 댓글을 못 단다고?"
내 블로그를 읽을 사람들 중 GitHub 계정이 있는 비율이 얼마나 될까.
친구들,의사, 부모님들... 대부분 없을 것이다.
"Claude, GitHub 계정 없이도 댓글 달 수 있는 방법 없어?"
"Supabase를 쓰면 됩니다. 데이터베이스를 직접 만들고, 세션 기반 익명 댓글을 구현하면 돼요."
처음 듣는 이름이었다. 하지만 "누구나 댓글을 달 수 있다"는 말에 마음이 움직였다.
테이블 설계
CREATE TABLE comments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
post_slug TEXT NOT NULL,
author_name TEXT NOT NULL,
content TEXT NOT NULL,
parent_id UUID REFERENCES comments(id),
session_id TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
post_slug: 어떤 글의 댓글인지 parent_id: 답글이면 부모 댓글 ID session_id: 브라우저에 저장된 익명 세션 (수정/삭제 권한용)
Claude와 함께 설계하면서 처음으로 "아, 이래서 DB 설계가 중요하구나" 싶었다.
모듈화의 집착
댓글 시스템 코드가 점점 커지자, Claude가 제안했다.
"이 정도 규모면 모듈화하는 게 좋겠어요."
src/features/comments/
├── types.ts # 타입 정의
├── api.ts # Supabase API 함수
├── hooks/
│ ├── useComments.ts # 조회 훅
│ └── useCommentMutations.ts # CUD 훅
└── components/
├── CommentSection.tsx
├── CommentItem.tsx
└── CommentForm.tsx
솔직히 처음엔 "이게 왜 필요해? 그냥 한 파일에 다 쓰면 안 돼?"라고 생각했다.
하지만 3일 후, 버그를 고칠 때 깨달았다.
"어디를 고쳐야 하는지 바로 알겠네."
Day 1 마무리: 첫 배포
Cloudflare Pages
오후 11시.
로컬에서는 완벽하게 돌아갔다. 이제 세상에 공개할 차례.
Cloudflare Pages를 선택했다. 무료이면서 빠르다고 했다.
npm run build
빌드 성공. /out 폴더에 정적 파일들이 생겼다.
Cloudflare에 GitHub 저장소를 연결하고, 빌드 명령어를 입력하고, 배포 버튼을 눌렀다.
1분 후.
"Your site is live!"
https://sol-blog-xxx.pages.dev
내가 만든 사이트가 인터넷에 떴다.
심장이 빨리 뛰었다.
"근데 댓글이 안 보여?"
기쁨도 잠시.
배포된 사이트에서 댓글이 표시되지 않았다. 로컬에서는 잘 되는데.
로그를 확인했다.
Error: supabaseUrl is required
"아..."
환경 변수 문제였다. 로컬에서는 .env.local 파일에서 읽었는데, Cloudflare에는 환경 변수를 따로 설정해줘야 했다.
Cloudflare 대시보드 → Settings → Environment Variables → 추가.
재배포.
댓글이 나타났다.
자정이 넘어가고 있었다.
Day 2: 검색과 RSS
"검색 기능도 넣어드릴까요?"
Day 2 아침, 기능 점검을 하고 있었다.
글 목록, 상세 페이지, 댓글... 기본적인 것들이 돌아가고 있었다.
그때 Claude가 먼저 제안했다.
"블로그라면 검색 기능이 있으면 좋을 것 같은데요. Pagefind라는 도구가 있어요. Rust로 만들어져서 빠르고, 빌드 타임에 인덱스를 생성해서 서버가 필요 없어요."
"그런 게 있어? 그거 넣어줘."
npm install -D pagefind
빌드 스크립트에 한 줄 추가.
"build": "next build && npx pagefind --site ./out"
Cmd+K를 누르면 검색창이 뜨고, 타이핑하면 결과가 실시간으로 나타난다.
"내가 요청하지도 않았는데?"
Claude가 알아서 계획하고, 알아서 구현해줬다.
바이브 코딩의 묘미가 이런 거구나 싶었다. 내가 생각하지 못한 부분까지 AI가 채워준다.
RSS 피드 자동 생성
뉴스레터를 운영하려면 RSS 피드가 필요했다.
// scripts/generate-rss.mjs
function generateRSS(posts) {
return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Sol's Blog</title>
<link>https://solkim.blog</link>
${posts.map(post => `
<item>
<title>${post.title}</title>
<link>https://solkim.blog/archive/${post.slug}</link>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
</item>`).join('')}
</channel>
</rss>`
}
빌드할 때마다 자동으로 /rss.xml이 생성된다.
작은 스크립트 하나가 미래의 나를 엄청 편하게 해줄 것이다.
Day 2 마무리: 카테고리 구조 개편
글이 늘어날수록 관리가 어려워질 게 뻔했다.
기존 구조:
content/blog/published/
├── post1.md
├── post2.md
└── post3.md # 어떤 주제인지 모름
새로운 구조:
content/blog/published/
├── tech/
│ └── nextjs-guide.md
├── life/
│ └── career-change.md
└── parenting/
└── baby-sleep.md
폴더 이름이 곧 카테고리.
파일을 이동하면 카테고리가 자동으로 바뀐다.
"단순하지만 강력하네."
48시간의 성과
Day 2가 끝날 무렵, 완성된 것들:
- ✅ 옵시디언 마크다운 자동 변환 (위키링크, 이미지)
- ✅ 블로그 목록 및 상세 페이지
- ✅ Supabase 댓글 시스템 (완전 모듈화)
- ✅ Pagefind 검색 엔진
- ✅ RSS 피드 자동 생성
- ✅ 카테고리별 폴더 구조
- ✅ Cloudflare Pages 배포
- ✅ GitHub Actions CI/CD
진행률: 0% → 78.4%
이틀 만에 핵심 기능의 대부분이 완성되었다.
배운 것들
1. 작동하는 최소 버전의 힘
처음부터 완벽하게 만들려고 하지 않았다.
위키링크 변환? 기본적인 것만 먼저. 댓글 시스템? 작성/조회만 먼저. 검색? 제목 검색만 먼저.
일단 돌아가게 만들고, 나중에 개선했다.
이 접근법이 5일 완성의 비결이었다.
2. 모듈화는 미래의 나를 위한 선물
처음에는 귀찮았다.
"그냥 한 파일에 다 쓰면 안 돼?"
하지만 3일 후, 버그를 고칠 때 깨달았다.
잘 정리된 코드는 수정이 쉽다.
3. AI는 내가 생각 못한 것도 제안한다
검색 기능은 내가 요청하지 않았다.
Claude가 먼저 "이런 건 어때요?"라고 물었고, 나는 "좋아, 넣어줘"라고 답했다.
바이브 코딩의 진짜 힘은 '대화'에 있다.
4. 환경 변수는 배포 전에 확인하자
로컬에서 되는데 배포하면 안 되는 경우, 십중팔구 환경 변수 문제다.
이것 때문에 1시간을 날렸다.
다음 편 예고
Day 3-4에서는 진짜 '디테일'의 영역으로 들어간다.
- 타이포그래피 개선
- 코드 블록 복사 버튼
- Lightbox 이미지 뷰어
- 보안 감사
- macOS nvm PATH 문제 (이건 진짜 삽질이었다)
기능은 48시간 만에 완성했지만, '프로덕션 레벨'이 되기까지는 더 많은 시간이 필요했다.
관련 링크
이 글이 도움이 되었나요?
관련 글

코딩 모르는 의사가 5일 만에 블로그를 만든 이유
206개의 커밋, 66개의 PR, 5일간의 몰입. 네이버 블로그 셋방살이를 청산하고 나만의 집을 지은 이야기

Day 5: 런칭, 그리고 5일간의 여정에서 배운 것들
206개의 커밋, 66개의 PR, 60시간의 몰입. 6개월 전 가이드가 안 맞고, 마지막 순간에 타입 에러가 터지고, 그래도 결국 런칭 버튼을 눌렀다

Day 3-4: 디테일이 만든 차이 - 48시간의 삽질 기록
Claude가 제안한 개선점들을 구현하다 만난 예상치 못한 벽들. Lightbox 재클릭 버그로 새벽 3시까지 삽질하고, macOS PATH 문제로 5시간을 날린 이야기