Claude Code로 CLI 훈련 플랫폼 만들고 배포하기까지

sunvelop·2026년 4월 7일

"AI한테 명령어 물어보다가 장애 대응 실패할 뻔했다"

새벽에 서버 장애가 터졌다. SSH로 급하게 접속하고, 컨테이너 로그를 봐야 하는데 손이 멈췄다.

docker logs 다음에 뭘 치지? --tail이 먼저인가, -f가 먼저인가?

5초가 5분처럼 느껴지던 그 순간, 뭔가 잘못됐다는 걸 알았다. AI가 대신 해주는 것과 내가 직접 할 수 있는 건 완전히 다른 거였다.

그래서 만들기로 했다. CLI 명령어를 직접 손으로 훈련하는 플랫폼을. 그리고 아이러니하게도, 그 플랫폼을 Claude Code와 함께 만들었다.


Claude Code를 어떻게 썼는가

이 글은 DevStand 소개이기도 하지만, 더 솔직하게는 Claude Code를 페어 프로그래머로 쓴 경험에 대한 이야기다.

Claude Code는 자동완성이 아니다. 터미널에서 직접 실행하면서 프로젝트 파일을 읽고, 수정하고, 빌드하고, 테스트를 돌린다. "이거 이렇게 해줘"라고 하면 파일을 열고 바꾸고 커밋 메시지까지 제안한다. 쓰면 쓸수록 어떻게 맥락을 넘기느냐가 핵심이라는 걸 알게 됐다.


CLAUDE.md: AI에게 팀의 컨텍스트를 넘기는 법

두 대의 맥북을 번갈아 쓰며 개발했다. Claude Code의 로컬 메모리는 기기 간 공유가 안 된다. 그래서 선택한 방식이 CLAUDE.md 파일이었다.

세션이 끝날 때마다 sessions/2026-03-10_MVP완성.md 같은 파일을 만들었다. 오늘 무엇을 했고, 어떤 결정을 내렸고, 다음 세션에서 뭘 해야 하는지. 그걸 git에 커밋하면 다른 기기에서도 같은 맥락을 이어갈 수 있었다.

Claude Code가 새 세션을 시작할 때 이 파일들을 읽으면서 "아, 저번엔 이런 거 했었군" 하고 이어가는 방식이다. 마치 팀에 새로 합류한 개발자에게 온보딩 문서를 주는 것처럼.


서브에이전트 패턴으로 배포 전 점검

배포 직전, 코드 전체를 한 번 더 검토하고 싶었다. 혼자 보면 한계가 있으니까. Claude Code에게 역할을 나눠서 시뮬레이션을 돌렸다.

"백엔드 아키텍트, 프론트엔드 아키텍트, DevOps 엔지니어 3명이 이 코드를 리뷰한다고 가정하고, 각자의 관점에서 문제점과 개선점을 알려줘."

30분 뒤에 14페이지짜리 보고서가 나왔다. 각 전문가별 점수와 P0(즉시 수정) / P1(배포 전 권장) / P2(리팩토링) 분류까지. 종합 점수 8.0/10.

그중 몇 가지는 놓쳤으면 배포 후 장애가 됐을 것들이었다.

백엔드 아키텍트가 잡아낸 것들:

  • Swagger UI가 프로덕션에서도 열려 있음 → API 스펙 노출
  • AdminService에서 N+1 쿼리 발생 → JOIN FETCH로 수정
  • 랭킹 조회 API에 Rate Limiting IP 우회 가능성

DevOps 엔지니어가 잡아낸 것들:

  • HikariCP 커넥션 풀 설정이 기본값 → 프로덕션 트래픽 대응 불가
  • submitAnswer 트랜잭션에 timeout 미설정 → DB 커넥션 고갈 가능성

이걸 P0~P2로 분류해서 하나씩 처리했다. 총 16건, 커밋 4개.

e1b022f fix: i18n 번역 품질 개선 (영문 5건 + 일문 3건)
177e85e fix: P0 프로덕션 필수 개선 7건
a3f6bb5 fix: P1 코드 품질 개선 7건
7c060ad refactor: P2 Object[]→인터페이스 projection 전환 + 테스트 추가

혼자 개발하면 이런 검토를 건너뛰기 쉽다. "일단 배포하고 보자" 하는 마음이 드는데, 이 방식은 그걸 막아줬다.


Flyway와 Docker Compose 변수 충돌 — 혼자였으면 반나절 날렸다

CLI 훈련 플랫폼이다 보니 DB에 명령어 데이터가 대량으로 들어간다. Docker Compose 명령어 예시들이 Flyway 마이그레이션에서 충돌이 났다.

Flyway가 SQL 안의 ${DB_PORT:-5432} 같은 문자열을 플레이스홀더로 해석해서 치환하려고 하는데, 변수가 없으니 실패하는 것.

Claude Code와 이 에러를 같이 보다가 원인을 잡았다. 해결책은 application.yml에 한 줄:

flyway:
  placeholder-replacement: false

이게 없으면 Docker Compose, Kubernetes, Terraform 카테고리 문제 전체가 마이그레이션 실패한다. 이 함정은 공식 문서에도 잘 안 나와 있는데, 에러 메시지를 같이 분석하면서 바로 찾아냈다.


stale closure와 IME — "직접 써보니까 발견됐다"

퀴즈 모드 외에 단순 반복 타이핑 모드가 필요하다고 생각했다. 점수나 피드백 없이 그냥 손에 익을 때까지 반복해서 치는 것. 타자 연습할 때처럼. 처음 타자 연습을 시작할 때를 떠올리면 — 어떻게 치는지 머리로는 아는데 손이 안 따라가던 그 경험. CLI 명령어도 마찬가지라고 봤다. 이해한다고 바로 몸에 배는 게 아니니까. 그래서 멈춤 없이 연속으로 타이핑하는 젠 모드를 만들었다.

그걸 직접 써보다가 이상한 걸 발견했다. 빠르게 타이핑하면 정확도가 100%인데 실제론 틀린 게 있는 것.

원인은 useState의 stale closure였다.

// 문제: 이전 render의 state를 참조함
const [accuracy, setAccuracy] = useState(100);

const handleKeyDown = (e) => {
  if (e.key !== expectedChar) {
    setAccuracy(accuracy - 1); // accuracy가 stale할 수 있음
  }
};

빠른 타이핑에서는 이전 렌더링의 accuracy를 참조해서 업데이트가 씹혔다. 해결은 useRef로 동기 추적하는 방식.

const accuracyRef = useRef(100);

const handleKeyDown = (e) => {
  if (e.key !== expectedChar) {
    accuracyRef.current -= 1; // 항상 최신값
  }
};

그리고 한국어 IME 문제도 있었다. 영어 CLI 명령어를 치는 서비스인데, 한글 입력 모드에서 타이핑하면 전혀 다른 문자가 들어온다. 방어 계층을 세 겹으로 넣었다:

  1. isComposing 체크 — IME 조합 중이면 입력 무시
  2. charCode > 127 — 비ASCII 문자 차단
  3. "영문 입력 모드로 전환하세요" 안내 메시지

이런 엣지케이스들은 문서를 아무리 읽어도 모른다. 직접 써봐야 나온다. Claude Code랑 같이 하면서 이런 버그를 발견하면 바로 "왜 이런 게 생기는 거야?"하고 물어볼 수 있었고, 대부분 바로 원인과 해결책이 나왔다.


문제 설계: 숫자보다 구조가 중요했다

처음부터 "2,633문제"를 목표로 잡은 게 아니었다. 문제는 결과고, 중요한 건 어떤 구조로 학습하게 할 것인가였다.

가장 먼저 부딪힌 질문: "명령어를 외우면 뭐가 달라지나?"

단순 암기는 금방 잊힌다. 왜 이 명령어가 필요한지, 언제 쓰는지를 모르면 실무에서 꺼내 쓰지 못한다. 그래서 각 문제에 3계층 구조를 붙였다.

WHAT — 이 도구가 뭔지: 개념 설명. 이미 아는 사람은 접어두면 된다.

WHY — 실무에서 왜 쓰는지: 이게 핵심이다. "마이크로서비스 환경에서 수백 개 컨테이너의 네트워크를 격리해야 할 때" 같은 실무 배경이 있어야 명령어가 맥락으로 머릿속에 남는다. 맥락이 있는 기억은 훨씬 오래 간다.

HOW — 직접 해보기: 퀴즈 + 틀렸을 때 상세 해설 + 예상 출력. 정답만 보여주는 게 아니라 왜 그게 정답인지를 설명한다.

다음 질문: "정답 비교를 어떻게 할 것인가?"

CLI 명령어는 플래그 순서가 달라도 동작이 같은 경우가 많다. docker run -d -p 8080:80 nginxdocker run -p 8080:80 -d nginx는 같은 명령이다. 단순 문자열 비교로는 한쪽만 정답 처리된다.

서버 사이드에서 명령어를 토큰 단위로 파싱해서 구조적으로 비교하는 방식을 택했다. base command, subcommand, 옵션(-d--detach 매핑), 인자를 각각 분해해서 비교한다. 이 덕분에 사용자가 어떤 순서로, 어떤 축약형으로 입력해도 정답 처리가 된다.

난이도도 설계가 필요했다. Lv.1부터 Lv.5까지, 단순히 어렵고 쉬운 게 아니라 플래그 개수, 조합 복잡도, 실무 빈도를 기준으로 분류했다.

이 구조를 잡고 나서, Claude Code와 함께 각 카테고리별로 문제를 채워나갔다. Docker, Kubernetes, Linux, Git, Terraform, AWS CLI, Helm, Podman, Redis CLI, Ansible, Nginx... 숫자는 그 과정의 결과다. 구조가 먼저였다.


Claude Code와 협업하면서 바뀐 것

세 달 정도 혼자 + Claude Code로 이 프로젝트를 진행하면서 느낀 것들.

가장 달랐던 건 "막힘"의 질감이었다. 이전에는 에러가 나면 검색 → 스택오버플로우 → 블로그 → 공식 문서 순서로 돌아다녔다. 지금은 에러 메시지를 그대로 보여주고 "이게 왜 나지?"라고 물으면 원인 분석이 바로 나온다. 단순히 빠른 게 아니라, 내 코드의 맥락을 알고 있으니 일반적인 답이 아닌 내 상황에 맞는 답이 나온다는 게 달랐다.

CLAUDE.md와 sessions 파일 덕분에 맥락 연속성도 생각보다 잘 됐다. 새 세션을 열어도 파일들을 읽으면서 "어 저번에 이거 했었지" 하고 이어갈 수 있었다. 프로젝트 기간이 길어질수록, 특히 두 대 기기를 오가며 개발할수록 이게 더 중요해졌다.

아쉬운 것도 있다. 결국 최종 판단은 내가 해야 한다. Claude Code가 제안하는 걸 무비판적으로 받아들이면 안 된다. 비즈니스 로직이나 아키텍처 결정은 내가 이해하고 선택해야 한다. 컨텍스트 창 한계도 있다 — 프로젝트가 커질수록 한 세션에 담을 수 있는 맥락이 제한된다. CLAUDE.md를 잘 관리해야 하는 이유다.

그럼에도, 혼자 개발하는 고독감이 확실히 줄었다. "이거 왜 이렇게 됩니까?" 하고 바로 물어볼 수 있는 존재가 옆에 있다는 게, 막힐 때마다 검색창만 뒤지던 것과는 경험 자체가 다르다.


마치며

결국 이 프로젝트를 만든 이유는 단순했다. 검색해서 아는 것과, 손이 먼저 움직이는 건 다르다는 걸 직접 겪었기 때문이다.

Claude Code와 함께 만들었지만, 목표는 오히려 반대였다. AI에게 물어보지 않아도 되는 순간을 늘리는 것. 새벽에 장애가 터졌을 때도, 머리보다 손이 먼저 움직일 수 있도록.

어제보다 손이 조금씩 더 잘 움직이는 게 목표다.

https://devstand.dev

0개의 댓글