donghakim.dev — zsh
← ls ../blog

Claude 로 개인 블로그를 만든 과정

Claude Design 과 Claude Code 로 디자인부터 배포까지 진행한 과정입니다.

Claude 로 개인 블로그를 만든 과정 · cover

코드를 직접 작성하지 않고, 집에 있는 미니 PC 에서 동작하는 개인 블로그를 구축한 기록입니다. 디자인 시안부터 TLS 갱신까지 사람은 의도를 결정하고 결과를 검수했습니다.


왜 또 만들었나

"개인 블로그 한 번 만들어 봐야지" 라는 생각은 개발자라면 누구나 한 번쯤 합니다. 거기서 조금 욕심을 부렸습니다.

AI를 써서, 디자인부터 셀프 호스팅까지 한 줄도 직접 안 짜고 끝낼 수 있을까?

코드는 결과물이고, 결과물보다 더 보고 싶은 것은 작업 흐름이었습니다. 그동안 직접 코드를 작성해 본 적 없는 영역(Next.js, 인프라, 디자인) 까지 한 번에 다뤄 보려면 사람이 코드를 입력하는 속도가 가장 큰 병목입니다. 그 병목이 없으면 작업이 어떻게 진행되는지 보고 싶었습니다.

결과부터 기술하면, 이 사이트의 백엔드·프론트엔드·인프라·CI/CD·디자인 시스템까지 제가 직접 작성한 코드는 없습니다. 모든 라인은 Claude 가 작성했고, 저는 의도를 결정하고 결과물을 검수했습니다.


디자인: 대화로 컨셉을 정한다

먼저 Claude 웹 쪽과 디자인 대화를 길게 했습니다. 결정해야 할 항목은 사실 몇 개 안 됩니다.

  • 톤: "정돈된 개발자 노트북" 같은 느낌으로 결정했습니다. 디자인 자체가 콘텐츠를 가리지 않아야 합니다.
  • 모티프: 터미널 프롬프트, ASCII 룰, $ ls -t ./blog/*.md 같은 커맨드 라인 헤더를 사용합니다.
  • 팔레트: paper-0..4 / ink-0..3 두 축으로만 구성해, 라이트/다크/세피아 테마가 같은 스케일을 공유하도록 했습니다.
  • 타이포: 본문은 Spectral serif, 코드와 강조는 IBM Plex Mono 입니다. 한글은 Plex Sans KR 로 fallback 을 고정했습니다(mono 폰트의 한글이 시스템 명조로 대체되는 것을 방지하려고).

이 결정들을 Claude 한테 컨셉 보드 형태로 정리해 달라고 했고, 그 내용을 그대로 CSS 변수 토큰으로 정의했습니다. 디자인 시스템이 의도대로 적용되는지는 컴포넌트를 하나씩 화면에 표시해 보며 확인했습니다. 색·간격·폰트를 직접 결정하지 않고 어떤 느낌이어야 하는지만 지정해 줘도 일관된 시안이 생성됐습니다.


백엔드: Spring Boot 4 + Kotlin + 헥사고날

설계 단계부터 Claude 에게 위임했습니다.

  • Spring Boot 4 · Kotlin 2.1 · JDK 25 · JPA · MySQL 8.4
  • 헥사고날 아키텍처. domain / application / adapter(in/out) 로 분리하고, 도메인 패키지에 Spring·JPA 의존이 포함되지 않도록 했습니다(나중에 ArchUnit 으로 강제하기로 했습니다).
  • Spring Security + JWT. Access/Refresh 토큰을 분리하고, 어드민 라우트는 ROLE_ADMIN 만 접근할 수 있게 했습니다.
  • Flyway. 마이그레이션은 추가만 하고, 기존 V 파일은 절대 수정하지 않습니다. checksum 이 일치하지 않으면 그 자리에서 부팅이 실패하기 때문입니다.
  • Testcontainers. 테스트가 MySQL 컨테이너를 자동으로 기동합니다. 로컬에 실제 DB 가 없어도 테스트가 실행됩니다.

처음부터 "이 도메인을 추가해 줘" 한 줄로 시작하지 않았습니다. 컨텍스트가 축적되도록 작은 단위로 분할해 요청했습니다. 도메인 모델 → 포트 → 어댑터 → 컨트롤러 → DTO 순으로 분할하고, 각 단계에서 변경된 파일만 PR 처럼 검토했습니다. 사람이 판단해야 하는 부분은 경계 결정입니다. "프로필은 단일 레코드인가, 시계열인가?" 같은 질문은 모델이 추측하지 않도록 제가 미리 답을 결정해 줘야 합니다.

엔드포인트는 공개 GET 과 어드민 CRUD 로 명확히 구분되어 있고, 어드민 쪽은 모두 @PreAuthorize("hasRole('ADMIN')") 로 접근이 제한되어 있습니다. 시드 관리자는 첫 부팅 때 환경변수로만 생성되고, 그 뒤로는 env 에서 제거해도 DB 에 유지됩니다.


프론트엔드: Next.js 15 + React 19, 서버 컴포넌트로 얇게

프론트 쪽은 더 단순합니다.

  • Next.js 15 App Router · React 19 · TypeScript 6. 서버 컴포넌트로 데이터를 SSR 단계에서 조회하고, 인터랙션이 필요한 부분만 'use client' 로 분리했습니다.
  • react-markdown + rehype-highlight + remark-gfm + mermaid. 본문은 마크다운, 코드는 highlight.js, 다이어그램은 mermaid 로 렌더링합니다.
  • 어드민 콘솔. /admin 하위에 dashboard / posts / pages / media 4 그룹이 있습니다. 글 작성 화면은 자동 저장(useAutosave) 으로 새로고침해도 작성 중인 내용이 유지됩니다.
  • Amplitude. 트래픽이 거의 없는 사이트지만, AI 가 어떤 부분을 잘못 구현한 채로 배포됐는지 제가 모르는 상태가 가장 걱정되어서 클릭/검색/태그 필터/스크롤 깊이 정도는 모두 이벤트로 기록합니다.

여기서도 작은 단위로 분할해 요청하는 것이 핵심이었습니다. "블로그 목록 페이지 만들어 줘" 가 아니라 "태그 카운트 0 인 것은 노출 제외, 활성 태그 재클릭은 해제 토글" 처럼 동작 명세로 전달했습니다. 모델이 "그럴듯하지만 빗나간" 코드를 작성할 여지를 축소하는 가장 비용이 적은 방법이었습니다.


인프라: 미니 PC 한 대로 셀프 호스팅

배포 타깃은 집에 있는 미니 PC 입니다. 이미 immich / n8n / Obsidian sync 같은 자가 호스팅 서비스들을 같은 패턴으로 운영하고 있어서, 같은 구조에 서비스를 추가하기만 하면 됐습니다.

구조는 다음과 같습니다.

loading diagram…
  • Docker Compose. 백/프론트/DB 가 각자 컨테이너로 실행됩니다. 업로드 볼륨은 호스트 디스크로 마운트합니다.
  • nginx 컨테이너. 기존 reverse proxy 에 server 블록만 추가했습니다. proxy-network 라는 공용 도커 네트워크에 백/프론트가 참여해서 nginx 가 서비스명으로 호출합니다.
  • Let's Encrypt + certbot. webroot 방식으로 발급하고, 갱신은 cron 으로 실행합니다.
  • GitHub Actions CI/CD. push → 빌드 → 테스트 → Docker Hub 푸시 → SSH 로 미니 PC 접속 → docker compose pull && up -d --no-deps <service> 순서로 진행됩니다. 백/프론트 워크플로가 따로 실행되어서 한쪽 배포가 다른 쪽에 영향을 주지 않습니다.
  • 롤백. 이미지 태그를 커밋 SHA 로 지정해 두어서, 문제가 발생하면 TAG=<이전-sha> docker compose up -d 한 줄로 복구합니다.

이 인프라를 구성하면서 제가 직접 명령어를 입력한 적은 없습니다. Claude 에게 "기존 서비스들이 이런 패턴이니까, 새 서비스를 이 패턴에 끼워 줘" 라고 컨텍스트만 제공하면, GitHub Actions 워크플로 yaml, nginx server 블록, docker-compose 서비스 정의, 배포 런북 마크다운까지 한 번에 생성됩니다. 사람이 한 일은 시크릿(SSH 키, Docker Hub 토큰, JWT 시크릿)을 GitHub Secrets / .env 에 입력해 주는 것뿐이었습니다.


그래서 사람은 무엇을 했나

코드를 직접 작성하지 않았다고 해서 사람이 할 일이 없었던 것은 아닙니다. 시간을 사용하는 방식이 변화했습니다.

코드를 작성하는 대신 시간을 투입한 일은 다음과 같습니다.

  • 경계 결정. 도메인 모델, 권한 분리, 어떤 것이 단일 레코드이고 어떤 것이 리스트인지를 결정했습니다.
  • 검수. diff 를 한 줄씩 본다는 뜻이 아니라, "이 변경이 만든 동작" 이 의도대로인지를 직접 실행해서 확인했습니다. 특히 모바일 뷰는 빌드 결과만 봐서는 알 수 없습니다.
  • 보안 경계. 어드민 라우트, 업로드 경로, SecurityConfig permit 규칙, 시크릿이 절대 코드에 포함되지 않게 하는 룰을 수립했습니다.
  • 컨텍스트 관리. Claude Code 의 CLAUDE.md 와 memory 에 "이 프로젝트의 결정들" 을 기록해 둡니다. 다음 세션이 같은 결정을 다시 추측하지 않게 하기 위해서입니다.
  • 테스트 의도. 테스트 코드 자체는 AI 가 잘 작성합니다. 그런데 어떤 경계 조건이 의미 있는지는 사람이 지정해 줘야 합니다. 모델이 놓친 위험한 경계 조건을 발견해 전달하는 것은 사람이 해야 했습니다.

AI 가 코드를 작성하기 시작한 뒤로는, 요청하는 사람의 도메인 지식과 판단이 결과물의 품질을 결정하게 됐습니다. 코드를 직접 입력하는 속도는 더 이상 병목이 아니었습니다.


무엇을 배웠나

사이드 프로젝트 하나로 다 일반화하긴 어렵지만, 이번에 분명히 느낀 것은 다음과 같습니다.

  • "맡기는 단위" 가 결과 품질을 결정합니다. "블로그 만들어 줘" 와 "포스트 목록에서 태그 카운트 0 은 제외" 는 같은 모델한테 요청해도 결과 품질에 차이가 납니다. 사람이 동작을 먼저 명세할 수 있어야 합니다.
  • 컨텍스트는 모델 안이 아니라 프로젝트 파일에 기록합니다. memory, CLAUDE.md, README, DEPLOY 런북이 여기에 해당합니다. 다음 세션이 추측하지 않도록 보존해 둔 이 기록들이 모델의 결과 품질을 결정합니다.
  • 검수는 diff 가 아니라 실제 동작으로 합니다. UI 변경은 빌드를 통과해도 모바일에서 레이아웃이 깨질 수 있고, 백엔드 변경은 단위 테스트를 통과해도 운영 데이터에서 NPE 가 발생합니다. 사람이 직접 실행해 보고 조작해 봐야 하는 부분은 감소하지 않습니다.
  • 셀프 호스팅이 의외로 가성비가 좋습니다. 미니 PC 한 대로 immich, n8n, Obsidian sync, 이 블로그까지 운영합니다. AI 가 인프라까지 구성해 주면 클라우드 비용 없이 사이드 프로젝트를 유지할 수 있습니다.

다음에 할 일

  • ArchUnit 으로 도메인 패키지의 Spring/JPA 의존 금지 룰을 강제하기.
  • 댓글 시스템(모더레이션 포함) 개발하기. 이것도 AI 에게 위임할 작업입니다.
  • Lighthouse 점수 모니터링을 GitHub Actions 에 추가하기.

P.S.

사실 이 글도 AI 가 작성했습니다.

프로젝트 구조와 결정들을 컨텍스트로 제공하고, "프로젝트 결정 과정이 드러나게, 보안 정보는 빼고 써 줘" 한 줄로 요청했습니다. 제가 한 일은 이번에도 의도를 결정하고 결과물을 검수한 것뿐입니다.

#ai#claude-code#devops#docker#nextjs#self-hosting#side-project#spring