Claude Code로 웹사이트를 구축한 방법
요약
Claude Code를 개발 파트너로 활용하여 프레임워크 없이 PHP와 Vanilla JS만으로 웹사이트를 구축한 사례를 소개합니다. AI 보조 개발의 강점과 약점을 파악하기 위해 아키텍처 설계부터 구현까지 직접 제어하며 진행한 과정을 다룹니다.
핵심 포인트
- Claude Code를 활용한 AI 보조 개발의 실무 적용 경험 공유
- 프레임워크 없이 커스텀 MVC와 순수 기술 스택으로 프로젝트 구축
- 설계와 전략은 개발자가, 실제 코드 구현은 Claude가 담당하는 협업 방식
- 엄격한 계층 분리(Controller, Service, Model)를 통한 아키텍처 유지
저는 실제 작업 기간 약 3주에 걸쳐 이 웹사이트를 처음부터 구축했습니다. 휴가 기간 중 일주일, 몇 번의 주말, 그리고 그 사이의 몇몇 늦은 밤들을 활용했습니다. WordPress도, Laravel도, React도, Tailwind도 사용하지 않았습니다. 오직 PHP, vanilla JavaScript (순수 자바스크립트), 커스텀 CSS 디자인 시스템, 그리고 개발 파트너로서의 Claude Code만을 사용했습니다.
여기에 전체 이야기가 담겨 있습니다: 무엇을 만들었는지, 어떻게 만들었는지, 무엇이 효과적이었고 무엇이 그렇지 않았는지, 그리고 제가 무엇을 바꿀 것인지에 대한 내용입니다.
왜 처음부터 구축하는가?
프레임워크와 플랫폼 기반으로 구축된 제품들을 수년간 관리해 온 후, 저는 그 위에 놓인 추상화 계층뿐만 아니라 실제로 내부(under the hood)에서 무엇이 작동하고 있는지 이해하고 싶었습니다.
하지만 또 다른 이유가 있었습니다. 이제 AI 보조 개발 (AI-assisted development)은 어디에나 있으며, 저는 무언가를 관리하거나 업무에서 활용하기 전에 그것을 먼저 이해해야 한다고 강력하게 믿습니다. 그 강점과 약점을 직접 경험해 보아야 합니다. Claude Code로 실제 프로젝트를 구축하는 것은 AI가 실무에서 어떻게 작동하는지 배우는 과정이었습니다. 코드를 작성하는 것뿐만 아니라 계획, 설계, 전략 수립, 그리고 문제 해결까지 포함됩니다.
그래서 몇 가지 규칙을 정했습니다:
- 백엔드 프레임워크 사용 금지. Laravel이나 Symfony를 사용하지 않습니다. 처음부터 직접 만드는 커스텀 MVC를 사용합니다.
- 프론트엔드 프레임워크 사용 금지. React, Vue, jQuery를 사용하지 않습니다.
- CSS 프레임워크 사용 금지. Tailwind나 Bootstrap을 사용하지 않습니다. 토큰 (tokens)을 활용한 커스텀 디자인 시스템을 사용합니다.
- 제대로 구축하기. 데이터베이스 마이그레이션 (Database migrations), 서비스 레이어 (service layer), 피처 플래그 (feature flags), 배포 파이프라인 (deployment pipeline)을 포함합니다. 단순한 장난감 프로젝트가 아닙니다.
- 구현에는 Claude Code 사용. 모든 아키텍처 및 설계 결정은 제가 내립니다. Claude는 코드를 작성합니다.
기술 스택 (The Stack)
이 사이트를 구동하는 요소들:
| 계층 (Layer) | 기술 (Technology) | 세부 사항 (Details) |
|---|---|---|
| 백엔드 (Backend) | PHP 8.2+ | 엄격한 타이핑 (strict typing)을 적용한 커스텀 MVC 프레임워크 |
| ... |
npm을 사용하지 않았습니다. 프론트엔드를 위한 Composer 패키지도 사용하지 않았습니다. 제가 직접 작성한 PHP 미니파이어 (minifier) 외에는 빌드 도구도 사용하지 않았습니다.
아키텍처 (Architecture)
요청 흐름 (request flow)은 간단합니다:
요청 (Request) → index.php → Bootstrap → Router → Controller → View
↓
Service → Model → Database
하지만 흥미로운 부분은 제가 강제한 제약 사항들입니다.
엄격한 계층 분리 (Strict Layer Separation)
컨트롤러 (Controllers)는 HTTP를 처리합니다. 그게 전부입니다. 컨트롤러는 요청 데이터를 추출하고, 인증을 확인하며, 서비스 (services)나 모델 (models)을 호출합니다. 비즈니스 로직 (business logic)은 절대 포함하지 않습니다.
서비스 (Services)는 모든 변경 (mutations)을 처리합니다. 모든 생성 (create), 수정 (update), 삭제 (delete)는 입력값을 검증하고, 슬러그 (slugs)를 생성하며, 캐시 (cache)를 관리하고, ServiceResult 객체를 반환하는 서비스를 거쳐 진행됩니다. 컨트롤러는 쓰기 작업을 위해 모델을 직접 건드리지 않습니다.
모델 (Models)은 영속성 (persistence)을 처리합니다. 모든 모델은 BaseModel을 상속받으며, 허용된 컬럼을 위한 $fillable 화이트리스트를 정의하고, 오직 준비된 문 (prepared statements)만을 사용합니다. 모델 외부에서는 그 어디에서도 생 SQL (raw SQL)을 사용하지 않습니다.
// 컨트롤러 (Controller) (얇은 계층 - HTTP 관련 사항만 처리)
$result = BlogPostService::create($data);
if ($result->failed()) {
...
이 패턴은 코드베이스를 예측 가능하게 만들었습니다. 무언가 고장 나면, 정확히 어느 계층을 살펴봐야 할지 알 수 있습니다.
기능 플래그 (Feature Flags)
모든 콘텐츠 유형은 .env 파일에 기능 플래그 (feature flag)를 가집니다. 블로그 (Blog), 도구 (tools), 가이드 (guides), 여정 (journeys), API 명세 (API specs) 등이 해당됩니다. 플래그가 비활성화되면 라우터 (router)는 일반 방문자에게 "Coming Soon" 페이지를 보여주지만, 관리자 패널 (admin panel)에는 여전히 모든 것이 표시됩니다. 이는 새로운 섹션을 비공개로 구축하고 데이터를 채운 다음, 스위치를 올려 바로 출시할 수 있음을 의미합니다.
FEATURE_BLOG=true
FEATURE_TOOLS=true
FEATURE_GUIDES=true
...
데이터베이스 마이그레이션 (Database Migrations)
모든 스키마 (schema) 변경에는 UP 및 DOWN 섹션이 포함된 마이그레이션 (migration) 파일이 있습니다. 마이그레이션을 앞으로 실행하거나, 마지막 배치를 롤백 (roll back)할 수 있으며, 무엇이 적용되었는지 추적할 수 있습니다. CLI 또는 관리자 UI를 통해 작동합니다.
현재 40개의 마이그레이션이 적용되었습니다. 몇 번의 반복 (iterations) 후에 마이그레이션 시스템을 도입했기 때문에, 실제 스키마 변경 횟수는 추적된 횟수보다 더 많습니다. 하지만 초기 블로그 스키마부터 OAuth 테이블, 분석 (analytics), 콘텐츠 동기화 (content sync)에 이르기까지, 그 이후의 모든 변경 사항은 추적 가능하며 되돌릴 수 있습니다.
콘텐츠 파이프라인 (Content Pipeline)
블로그 포스트는 Markdown으로 작성되어 데이터베이스에 저장되며, 런타임 (runtime) 시점에 HTML로 렌더링 (render) 됩니다. 사전 렌더링된 캐시 (pre-rendered cache)는 없습니다. 콘텐츠 동기화 시스템은 모든 것을 JSON 번들 (JSON bundle, ID 기반이 아닌 슬러그 (slug) 기반)로 내보내며, 이는 프로덕션 (production) 환경에 임포트 (import) 될 수 있습니다. 조회수와 분석 데이터는 동기화 중에 절대 덮어쓰이지 않습니다.
브루탈리스트 디자인 (The Brutalist Design)
저는 "브루탈리스트 볼드 (Brutalist Bold)" 스타일을 선택했습니다. 어두운 배경, 모든 곳에 적용된 2px 테두리, 테두리 곡률 (border-radius) 0, 레이블과 메타데이터를 위한 모노스페이스 (monospace) 글꼴, 그리고 강조 색상으로 일렉트릭 라임 (Electric Lime, #D4FF00)을 사용했습니다.
주의 사항. 당신이 지금 읽고 있는 사이트는 이 모습이 아닙니다. v1 브루탈리스트 빌드는 2026년 4월에 Station warm-noir 디자인으로 교체되었습니다. 전체 리디자인 이야기(4일간, 1인, 엔드 투 엔드 (end-to-end))는 Four Calendar Days, One Person, One Full Redesign에서 확인할 수 있습니다. 이 포스트의 나머지 내용은 원본 v1 빌드 이야기이며, 있는 그대로 유지됩니다.
왜 브루탈리스트인가요? 두 가지 이유가 있습니다.
첫째, 제약 사항이 결정을 더 빠르게 만듭니다. 테두리 곡률 (border-radius)이 0이면, "이걸 4px로 할까, 8px로 할까?"라고 고민할 필요가 없습니다. 그림자 (shadow)를 허용하지 않으면 테두리 (border)를 사용하게 됩니다. 강조 색상이 하나뿐이라면 색상 팔레트 (color palette)를 짜는 데 시간을 낭비하지 않습니다. 제가 추가한 모든 규칙은 내려야 할 결정 하나를 줄여주었습니다.
둘째, 독특해 보입니다. 대부분의 사이트는 둥근 모서리와 부드러운 그라데이션 (gradient)에 의존하기 때문에, 날카로운 모서리는 눈에 띕니다. 이 사이트는 템플릿이 아니기 때문에 템플릿처럼 보이지 않습니다.
디자인 토큰 시스템 (The Design Token System)
모든 것은 tokens.css에 존재합니다. 색상, 간격 (spacing), 글꼴 크기, 테두리 두께, 트랜지션 (transition) 등 말이죠. 컴포넌트 (component)는 토큰 (token)만을 참조합니다. 하드코딩된 값 (hardcoded value)은 절대 사용하지 않습니다.
/* tokens.css */
--color-primary: #D4FF00;
--color-bg: #0a0a0a;
...
이 시스템은 CSS 커스텀 속성 (CSS custom properties)을 통해 다크/라이트 테마와 여러 가지 강조 색상 (라임, 사이언, 로즈)을 지원합니다. 테마를 전환한다는 것은 컴포넌트를 다시 작성하는 것이 아니라 토큰 값을 변경하는 것을 의미합니다.
v1 빌드는 48개의 CSS 파일과 15,500줄 이상의 코드로 구성되었습니다. 모든 컴포넌트는 각자의 파일에 담겼고, 유틸리티(utilities)는 컴포넌트를 덮어쓸 수 있도록 마지막에 로드되었습니다. 일반적인 개인 사이트에서 작성하는 것보다 훨씬 많은 CSS였지만, 토큰 시스템 덕분에 유지보수가 가능했습니다. (이후 진행된 v2 리디자인에서 많은 부분이 통합되었습니다. 현재 수치는 아래 표에 나와 있습니다.)
Claude Code와 함께 작업하기
모두가 이 부분에 대해 질문합니다.
나를 놀라게 한 것: 속도
그 속도는 제가 무언가를 구축하는 방식에 대한 생각을 바꾸어 놓았습니다. 하루 종일 걸릴 것이라고 예상했던 작업들이 한두 시간 만에 완료되었습니다. 단순히 상용구 코드(boilerplate)뿐만이 아니었습니다. Claude는 적절한 인덱싱 (indexing)을 포함한 복잡한 데이터베이스 쿼리, CSS 아키텍처 (architecture) 결정, 보안 패턴 (CSRF 토큰, prepared statements, rate limiting), 그리고 배포 파이프라인 (deployment pipeline) 설계까지 처리했습니다.
저는 제가 원하는 것을 평이한 언어로 설명하기만 하면 되었고, 즉시 작동하는 프로덕션 품질 (production-quality)의 코드를 돌려받을 수 있었습니다. "검증 (validation), 슬러그 생성 (slug generation), 캐시 무효화 (cache invalidation) 기능이 포함된 블로그 포스트용 서비스 레이어 (service layer)를 구축해줘." 완료되었습니다. "배치 (batch) 단위 롤백 (rollback)을 지원하는 마이그레이션 시스템 (migration system)을 만들어줘." 완료되었습니다.
마치 커피 휴식도 필요 없는, 빠르고 인내심 강한 시니어 개발자와 페어 프로그래밍 (pair programming)을 하는 기분이었습니다.
CLAUDE.md 파일
초기에 저는 Claude가 세션이 바뀌면 아키텍처 (architectural) 결정 사항을 "잊어버린다"는 사실을 발견했습니다. 이미 존재하는 버튼 스타일을 새로 만들거나, 우리가 정해둔 것과는 다른 에러 핸들링 (error handling) 패턴을 사용하곤 했습니다.
해결책은 프로젝트 루트에 상세한 CLAUDE.md 파일을 두는 것이었습니다. 디자인 토큰 (design tokens), 명명 규칙 (naming conventions), 아키텍처 규칙 (architectural rules), 컴포넌트 인벤토리 (component inventory) 등을 기록했습니다. Claude는 매 세션이 시작될 때 이 파일을 읽습니다. 또한 저는 CSS 표준부터 데이터베이스 컨벤션 (database conventions)까지 모든 것을 다루는 15개의 규칙 파일을 .claude/rules/에 생성했습니다. (이 내용은 Making Claude Code Work on Bigger Projects에서 더 자세히 다룹니다.)
이러한 지속적인 메모리(persistent memory)는 전환점이 되었습니다. 규칙이 존재하게 되자, Claude는 새로운 패턴을 만들어내는 것을 멈추고 이미 확립된 패턴을 따르기 시작했습니다.
Claude가 어려움을 겪었던 부분
과잉 엔지니어링 (Over-engineering). Claude는 필요 이상의 추상화 (abstraction)를 추가하는 경향이 있습니다. "그냥 간단한 함수 하나만 만들어줘"라고 요청하면, 추상 기본 클래스 (abstract base class) 하나와 두 개의 인터페이스 (interface), 그리고 팩토리 (factory)를 가져왔습니다. 저는 "더 단순하게"라고 말하는 데 많은 시간을 소비했습니다.
CSS 일관성 (CSS consistency). 엄격한 규칙이 없으면 Claude는 중복된 CSS 클래스를 생성하곤 했습니다. .card-regular가 이미 존재함에도 새로운 카드 컴포넌트를 만들거나, .btn--primary가 바로 옆에 있는데도 새로운 버튼 변형 (variants)을 만들었습니다. 규칙 파일이 이 문제를 해결해 주었지만, 여러 차례 중복된 스타일을 정리한 후에야 가능했습니다.
디자인 취향 (Design taste). Claude에게는 디자인 취향이 없습니다. 브루탈리즘 (brutalist) 미학은 전적으로 저의 비전이었습니다. Claude는 제가 원하는 것을 설명하자 잘 실행해 주었지만, "테두리 반경 (border-radius)이 없는 2px 테두리에 라임 그린 색상 포인트를 줍시다"라고 먼저 제안하지는 않았을 것입니다. 모든 시각적 결정은 저의 몫이었습니다.
두 가지 문제는 더 미묘했습니다. 첫 번째는 컨텍스트 드리프트 (context drift)였습니다. 많은 변경 사항이 발생하는 긴 세션 동안, Claude는 때때로 이전의 결정 사항들을 놓치곤 했습니다. 세션을 한 번에 하나의 작업에만 집중하도록 유지하는 것이 도움이 되었습니다. 규칙 파일도 마찬가지였습니다.
두 번째는 설명하기 더 어려운 문제입니다. 저는 이를 '멍청한 세션 (stupid sessions)'이라고 부르게 되었습니다. 가끔 Claude는 그냥... 작동을 멈추곤 했습니다. 충돌(crash)이 나거나 에러(error)가 발생하는 것이 아니었습니다. 응답은 계속했지만, 품질이 낭떠러지처럼 급격히 떨어졌습니다. 한 시간 전에는 잘 처리했던 간단한 작업들이 엉뚱한 결과물을 내놓았습니다. 세션 내내 따르고 있던 규칙들을 무시했습니다. 방금 수행한 작업과 모순되는 변경을 가했습니다.
제 이론은 이렇습니다: 컨텍스트 과부하 (context overload). 30~45분간 복잡한 작업을 수행하고 나면 무언가 고장이 납니다. 해결책은 놀라울 정도로 간단했습니다. 모든 것을 닫으세요. 자리를 뜨세요. 몇 시간 후에 돌아와서 새로운 세션을 시작하면, Claude는 아무 일도 없었다는 듯이 정확히 멈췄던 지점부터 다시 시작했습니다. 설명도, 사과도 없었습니다. 그저 다시 유능한 상태로 돌아왔을 뿐입니다.
그것은 하나의 리듬이 되었습니다. 30~45분 동안 집중해서 작업하세요. 만약 Claude가 당연히 알아야 할 것들을 다루기 시작하며 어려움을 겪는다면, 맞서 싸우지 마세요. 다른 결과를 기대하며 같은 프롬프트를 다섯 번씩 재시도하지 마세요. 그냥 멈추세요. 나중에 새로운 세션(Fresh session)을 여세요. 그 순간에는 답답하겠지만, 혼란에 빠진 AI와 한 시간 동안 논쟁하는 것보다 훨씬 빠릅니다.
잘 작동했던 부분
자연어 아키텍처 (Natural language architecture). 저는 "컨트롤러(controller)는 HTTP만 처리해야 하고, 서비스(service)는 비즈니스 로직을 처리해야 합니다"라고 말할 수 있었고, Claude는 자신이 생성하는 모든 컨트롤러와 서비스에 걸쳐 그 분리(separation)를 일관되게 구현했습니다.
대규모 리팩터링 (Refactoring at scale). "모든 카테고리 CRUD를 공유 트레이트(shared trait)로 추출하세요."라고 명령하면, Claude는 기존 구현을 읽고, 공통 패턴을 식별하고, 트레이트를 생성한 뒤, 두 서비스 모두를 업데이트했습니다. 단 한 번의 과정(one pass)으로 말이죠.
보안 패턴 (Security patterns). Claude는 제가 기대했던 것보다 보안에 더 뛰어났습니다. 컬럼 화이트리스트(Column whitelisting), prepared statements, CSRF 검증(validation), 속도 제한(rate limiting) 등, 제가 요청하지 않은 보안 조치들을 선제적으로 추가했습니다.
수치 (The Numbers)
3주간의 집중적인 작업으로 첫 번째 버전이 탄생했습니다. 아래 수치는 v2 재설계(redesign)와 그 초기 스프린트 이후 1년간의 반복 작업(iteration)을 거친 현재 사이트의 상태입니다:
| 지표 (Metric) | 수치 (Count) |
|---|---|
| PHP 파일 수 | 204 |
| ... |
개인 사이트에 왜 이렇게 많은 코드가 있는지
위 표를 합리적으로 읽어본다면 "왜 블로그에 약 48,700줄의 PHP 코드가 있는가?"라는 의문이 들 것입니다. 답은 블로그가 약 12개 정도의 기능 중 하나일 뿐이기 때문입니다. 이 수치들이 이 모든 것을 구동합니다.
공개 측면 (Public side). 카테고리, 태그, 관련 포스트, 전체 텍스트 검색(full-text search) 기능이 있는 블로그. 평점 및 클릭 추적(click tracking) 기능이 있는 도구 디렉토리. 모듈과 콘텐츠 게이팅(content gating)이 포함된 단계별 가이드. 기사 클러스터링(article clustering) 기능이 있는 RSS 기반 뉴스 애그리게이션(aggregation). About / Now / Contact 페이지. OAuth (Google, GitHub, LinkedIn) 및 매직 링크(magic-link) 로그인이 포함된 사용자 계정. SEO 인프라: 동적 사이트맵(dynamic sitemap), robots.txt, 구조화된 데이터(structured data), 캐노니컬 처리(canonical handling).
Admin, 빙산의 일각. 이곳에 대부분의 코드가 존재하며, 공개된 사이트보다 규모가 더 큽니다. 다음을 포함하는 21개의 어드민 컨트롤러 (admin controllers):
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
