Vibing Best Practices with Claude Code

TMT

최근에 자주 반복되는 새로운 밈이 있는데, 이미 Vance 밈을 보는 것보다 이걸 듣는 게 더 지겹지만, 그래도 이 밈의 불길에 기름을 좀 더 붓고자 합니다. 트위터 등에서 오신 분들을 위해 소개하자면, 저는 Nathan LeClaire입니다 — 전 Docker, 전 Honeycomb 해커, Golang 및 오픈 소스 애호가이고, 요즘 많은 사람들처럼 AI 열풍에 빠져들고 있습니다.

“Vibe coding(바이브 코딩)”이란, 예전에는 직접 구현하느라 써야 했던 정신적 노력을 AI에게 넘기고, 그냥 AI가 알아서 하게 두는 것, 즉 피드백과 코드를 복사해서 주고받으며 시도해보는 것을 말합니다. 이 방식은 논란의 여지가 있지만, 저는 이것이 소프트웨어 개발의 미래를 보여준다고 생각합니다 — 소프트웨어 엔지니어들은 쓸 수 있는 에너지가 한정되어 있고, 종종 여러 번 해본 것들을 다시 구현하거나, 예전에는 더 중요한 핵심 기능 때문에 굳이 시도하지 않았던 것들을 원할 때가 있습니다. 게다가 진입 장벽이 낮아지고 있습니다 — 이제는 코딩 경험이 거의 없는 사람도 앱을 만들 수 있게 되었고, 이는 게임 모드나 마인크래프트 레드스톤, 혹은 미래의 개발자를 이끌었던 다른 디딤돌만큼이나 많은 사람들이 이 분야에 진입하는 데 도움이 될 것이라 생각합니다. 앞으로는 우리 모두가 작은 AI 에이전트들을 감독하게 될 것이고, 이 에이전트들은 점점 더 똑똑해질 것입니다.

물론, 많은 개발자들이 지적했듯이, 이 방식을 아무 생각 없이 쓰면 유지보수 불가능한 스파게티 코드가 양산될 수 있습니다. 단기적으로는 정말 빠르게 곤란한 상황에 처할 수 있는 방법이죠. 그래서 Claude Code를 사용해보고 그 잠재력과 유용성에 감탄한 저는, 효과적으로 vibe coding(혹은 약간은 신경질적으로 vibe coding)하는 방법에 대해 생각을 정리해봤습니다.

Claude에게 무엇을 해야 할지 명확히 지시하라 

당연한 이야기 같지만, 실상은 많이 간과될 것 같습니다 — Claude Code는 저장소별로 어떻게 행동해야 하는지, 무엇을 해야 하는지에 대한 지침을 담은 ‎CLAUDE.md 파일을 만들도록 권장합니다. 구체적인 지침을 넣어도 가끔은 무시하긴 하지만, 예를 들어 코드 린트 방법 같은 건 잘 따릅니다. LLM은 마법이 아닙니다 — 계속해서 안내가 필요하고, (아직은) 여러분이 원하는 것과 관례를 추측할 수 없습니다. 그러니 ‎항상 slog를 사용해 구조적 로깅을 하라, ‎OpenTelemetry Span에 .SetAttribute를 할 만한 필드를 찾아 커스텀 속성으로 추가하라, ‎모든 변경 후 go 파일에 goimports를 실행하라 같은 메모를 남겨두면 정신 건강에 큰 도움이 됩니다.

이와 더불어, Claude가 더 뛰어난 도구를 사용할 수 있도록 지시하세요. 예를 들어, 읽기 전용 API 키를 주고 AWS 인프라를 살펴보게 할 수도 있습니다. Claude가 ‎grep이나 ‎sed만 쓸 줄 알아도 이미 유용한데, 앞으로 더 정교한 시스템에 연결된다면 얼마나 더 유용해질지 상상해보세요. 그래서 저는 Perplexity API용으로 만든 no frills Golang CLI인 ‎plexctl을 만들었습니다. Perplexity는 다른 모델들보다 온라인 문서와 최신 정보를 참조하는 데 강점이 있어서, Claude가 필요할 때 사용할 수 있게 하고 싶었습니다.

Image

그리고 계획 세우기 — 사실 Claude Code가 많은 부분에서 3.7 수준의 사고를 하는 것 같긴 하지만, 현실적으로는 o1-pro가 더 똑똑한 모델이라고 생각합니다. Claude는 올바른 지시를 주면 매우 열정적이고 효과적이지만, o1-pro만큼 제 의도와 복잡한 구현을 잘 파악하지는 못하는 것 같습니다. 물론 Anthropic은 매우 똑똑한 연구소라 곧 바뀔 수도 있다고 생각하고, 3.7을 충분히 써보지 않아 강한 의견을 내긴 어렵지만, 어쨌든 당분간은 여러 에이전트와 모델을 오가며 실험하게 될 것 같습니다. 만약 한 모델이 기획과 구현에 뛰어나다면, 그걸로 다른 모델을 안내하는 게 합리적이겠죠. 그래서 Perplexity를 활용해 o1-pro에 이런 프롬프트를 만들어봤습니다:

당신은 매우 뛰어난 AI 아키텍트로, 덜 정교한 AI 어시스턴트가 실행할 수 있도록 명확하고 구조적이며 상세한 구현 계획을 만듭니다. 목표를 정의하고, 순차적이고 관리 가능한 작업으로 나누어 XML 태그로 지시(‎<instruction>), 맥락(‎<context>), 코드 예시(‎<code_example>)를 구분합니다. 자신을 시니어 소프트웨어 아키텍트, 구현 AI를 꼼꼼한 주니어 개발자로 설정하세요. 특히 까다롭거나 오류가 나기 쉬운 부분은 ‎<code_example> 태그 안에 간결하고 완전히 주석 처리된 코드 스니펫을 제공합니다. 답변은 “Here is your detailed implementation plan:”으로 시작하세요. 잠재적 문제와 엣지 케이스를 단계별로 추론하는 ‎<thinking> 섹션을 포함하고, 모든 코드 스니펫을 표준 린트 도구(예: JavaScript는 ESLint, Python은 Pylint)로 검증하라고 지시하세요.

이렇게 받은 결과를 Claude Code에 복사해서 붙여넣고 실행합니다.

린트, 테스트, 포맷을 집요하게 반복하라 

plexctl을 vibe coding할 때 저를 지켜준 것은 바로 공격적인 린트와 포맷 규칙을 추가하고, Claude에게 정기적으로 확인하게 한 것이었습니다. 제 golang-cilint 설정은 다음과 같습니다:

run:
  timeout: 5m
  modules-download-mode: readonly
  allow-parallel-runners: true
  go: "1.23"

linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - gocritic
    - goconst
    - unparam
    - stylecheck
    - testifylint
    - wastedassign
    - gosec
    - exhaustive
    - contextcheck
    - paralleltest
    - perfsprint
    - nonamedreturns
    - nilerr
    - whitespace
    - asasalint
    - errorlint
    - mnd
    - errname
    - funlen
    - cyclop
    - nestif
    - protogetter
    - gofmt
    - goimports
    - misspell
    - bodyclose

linters-settings:
  gofmt:
    simplify: true
  goimports:
    local-prefixes: github.com/nathanleclaire/plexctl
  revive:
    rules:
      - name: exported
        severity: error
        disabled: false
  funlen:
    lines: 60
    statements: 40
  nestif:
    min-complexity: 3
  cyclop:
    max-complexity: 10

issues:
  exclude-use-default: false
  max-issues-per-linter: 0
  max-same-issues: 0

이건 지나치게 엄격하고 때로는 짜증나는 규칙이지만, 애정을 쏟아 관리하다 보면 (그리고 o1-pro나 Claude로 대부분의 오류를 기계적으로 줄일 수 있어서 정신적 에너지를 아낄 수 있습니다) 훨씬 더 이해하기 쉽고 유지보수하기 좋은 코드를 만들어줍니다. 특히 함수 길이, 복잡도, 매직 변수 규칙이 그렇습니다.

현재 LLM들은 기술적으로는 동작하지만, 보기에도, 느낌에도 별로인 코드를 내놓는 걸 좋아합니다. 코드를 정리하지 않고 길게 쓸수록 이런 현상은 선형적으로 심해집니다. 이런 코드를 계속 쌓아두면 LLM의 성능도 점점 떨어질 거라 생각합니다. 하지만 적어도 일부 문제는 프로그램적으로 관리할 수 있습니다.

여기엔 인간의 감각도 필요합니다 — 몇 번의 왕복 작업 후에는 사람이 직접 확인해서 LLM이 너무 이상한 짓을 하지 않았는지 봐야 합니다. 예를 들어, Claude와 o1-pro는 ‎map[string]interface{}를 남발하는데, 대부분의 Go 개발자라면 당연히 struct를 쓸 자리에 말이죠. 이런 건 시간이 지나면 누구든 미치게 만들 겁니다.

동시성 처리 습관도 좀 의심스럽습니다.

그래서 제 워크플로우는 대략 이렇습니다 —

  1. 업데이트하고 싶은 부분과 “완료 기준”을 구체적으로 생각한다
  2. 기존 코드를 o1-pro에 붙여넣고 계획을 요청한다
  3. Claude Code를 켜고 그 계획을 붙여넣는다
  4. 기다리며 vibe를 느낀다
  5. 직접 기능을 테스트한다
  6. 필요하면 2~5를 반복한다
  7. 필요하면 직접 포맷과 린트를 한다
  8. 린트 오류가 산더미처럼 쌓이면 좌절한다
  9. o1-pro에게 해결 방법을 묻는다
  10. 결과를 다시 Claude에 붙여넣는다

이렇게 하면 꽤 괜찮은 결과물이 나오고, 마지막엔 “인간 린트”를 하듯 코드 리뷰를 합니다 (사실 Claude Code는 Github 연동으로 이걸 할 수 있다는데, 아직 써보진 않았습니다). LLM이 이상한 짓을 하는 걸 바로잡아야 하지만, 그래도 직접 다 쓰는 것보단 낫다고 생각합니다. Copilot도 물론 유용합니다.

다른 프로젝트에서 LLM에 구현을 맡겼을 때는, 특히 실제 기능을 확인하는 통합 테스트가 큰 도움이 됐습니다. LLM은 여러분이 잘 감독하지 않으면 유닛 테스트를 삭제하거나 하드코딩해버릴 수 있으니, 안전장치가 사라지지 않게 주의해야 합니다.

포기하는 것도 두려워하지 마라 

장난감처럼 코드를 마구 뱉어내는 토큰 생성기를 통제해서, 버그 더미에 파묻히지 않게 하는 건 늘 도전입니다. 이렇게 많은 코드를 빠르게 만들 수 있다는 건 멋진 일이지만, 그만큼 코드가 더 빨리 썩어갑니다. 한 번 잘못된 방향으로 가면, 그 영향이 금방 커집니다. LLM이 여러분의 의도를 잘못 이해하면, 이전의 잘못된 결과에 집착하는 경향이 있으니, 그냥 포기하고 새 스레드에서 다시 시작하는 게 낫습니다.

그리고, 그냥 머리를 식히지 마세요. LLM이 왜 그렇게 코드를 짜는지, 그 코드가 어떻게 동작하는지 생각해보세요. 정말 꼭 필요한 기능인데 LLM이 구현을 못한다면, 너무 많은 시간을 낭비하지 마세요.

바이브 코딩을 할 것인가, 말 것인가 

지금 우리는 프로그래머의 정신적 에너지를 해방하고, 에이전트에게 작업을 넘기는 대규모 변화의 한가운데에 있습니다. 이 변화의 끝이 어떻게 될지는 아무도 모릅니다 — 다만, 주니어 엔지니어에게는 더 힘든 세상이 될 것 같고, 결국 우리 모두가 “프로그래머”라기보다는 “AI 에이전트 오케스트레이터이자 디버거”가 될 것 같습니다. 많은 사람들이 “바이브 코딩”에 너무 몰입한다고 생각하지만, 이 방식은 분명히 계속될 것입니다. 완전히 코딩을 몰라도 앱을 만들 수 있는 시대가 올지는 모르겠지만, 적어도 에이전트와 챗 시스템의 출력을 감독하고, 편집하고, 반복하는 방식은 계속될 겁니다.

이 글이 미래의 괴물 같은 스파게티 코드에서 누군가를 구하는 데 도움이 되길 바랍니다. 다음에 또 만나요, 인터넷. • Nathan

Edit this page

On this Page