Recent Posts

All Posts →

Go 코드에 품질 도구 적용하기

Go 프로젝트에서 기본적으로 갖추면 좋은 코드 품질 도구들(gofmt, goimports, go vet, golangci-lint)을 정리하고, Makefile로 통합하는 방법을 소개합니다.

Go는 언어 차원에서 코드 포맷팅 도구(gofmt)를 제공하고, 표준 도구 체인에 정적 분석(go vet)이 포함되어 있습니다. 이런 도구들을 프로젝트 초기에 설정해두면 코드 리뷰에서 스타일 논쟁을 줄이고, 흔한 실수를 빌드 전에 잡을 수 있습니다.

이 글에서는 Go로 개발하는 프로젝트에서 기본적으로 갖추면 좋은 도구들과 설정 방법을 정리합니다.

1. gofmt: 코드 포맷팅

Go 커뮤니티에서는 gofmt 이 표준 포맷터입니다. 탭/스페이스, 중괄호 위치 같은 스타일 논쟁 없이 모든 Go 코드가 동일한 포맷을 따르게 됩니다.

gofmt -w .

-w 플래그는 파일을 직접 수정합니다. -l 플래그를 쓰면 포맷이 맞지 않는 파일 목록만 출력합니다.

gofmt -l .

포맷이 맞지 않는 파일이 있으면 파일 경로가 출력되고, 모두 맞으면 아무것도 출력되지 않습니다. CI에서는 이 출력을 검사해서 포맷팅되지 않은 코드가 머지되는 것을 막을 수 있습니다.

2. goimports: import 정리

goimports는 `gofmt`의 기능에 더해 import 문을 자동으로 정리합니다.

  • 사용하지 않는 import를 제거

  • 필요한 import를 자동 추가

  • import를 표준 라이브러리 / 그 외 패키지 2그룹으로 정렬 (-local 플래그를 쓰면 로컬 패키지를 별도 그룹으로 분리 가능)

go install golang.org/x/tools/cmd/goimports@latest
goimports -w .

gofmt 대신 `goimports`만 실행해도 포맷팅까지 함께 처리됩니다.

3. go vet: 정적 분석

`go vet`은 Go 표준 도구 체인에 포함된 정적 분석 도구입니다. 컴파일러가 잡지 못하는 의심스러운 코드를 검출합니다.

go vet ./...

대표적으로 잡아주는 문제들은 다음과 같습니다.

  • fmt.Printf`의 포맷 문자열과 인자 불일치 (`printf)

  • 도달할 수 없는 코드 (unreachable)

  • sync.Mutex 등 잠금을 값으로 복사하는 코드 (copylocks)

  • context.WithCancel`의 cancel 함수를 호출하지 않는 경로 (`lostcancel)

별도 설치 없이 바로 사용할 수 있으므로, 모든 Go 프로젝트에서 기본으로 실행하는 것이 좋습니다.

4. golangci-lint: 통합 린터

golangci-lint는 여러 린터를 하나의 도구로 통합해서 실행해주는 린터 러너입니다. 개별 린터를 따로 설치하고 실행할 필요 없이, 설정 파일 하나로 원하는 린터를 선택해서 쓸 수 있습니다.

## 바이너리 설치 (go install은 공식적으로 권장되지 않음)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin
golangci-lint run ./...

4.1. 설정 파일

프로젝트 루트에 .golangci.yml 파일을 만들어 설정합니다. 아래는 golangci-lint v2 기준 설정 예시입니다.

.golangci.yml
version: "2"

linters:
  default: standard
  enable:
    - misspell

formatters:
  enable:
    - gofmt
    - goimports

v2에서는 `version: "2"`를 반드시 명시해야 합니다. `default: standard`로 설정하면 errcheck, govet, ineffassign, staticcheck, unused 5개 린터가 기본으로 활성화됩니다. 추가로 필요한 린터만 `enable`에 넣으면 됩니다.

v2의 또 다른 변경점은 gofmt, goimports 같은 포맷터가 linters`가 아닌 `formatters 섹션으로 분리된 것입니다. 기존 v1 설정 파일이 있다면 golangci-lint migrate 명령으로 자동 변환할 수 있습니다.

4.2. 기본 활성화 린터 (default: standard)

린터 설명

errcheck

에러 반환값을 무시하는 코드 검출

govet

`go vet`과 동일한 정적 분석

ineffassign

이후에 사용되지 않는 변수 할당 검출

staticcheck

Go 코드의 버그, 성능 문제, 스타일 문제를 종합적으로 검사 (gosimple, stylecheck 포함)

unused

사용되지 않는 변수, 함수, 타입 검출

이 중 `errcheck`은 특히 중요합니다. Go에서 에러를 반환값으로 처리하는 관례상, 에러를 무시하는 코드는 런타임에 예상치 못한 동작을 일으킬 수 있습니다.

5. Makefile로 통합

위 도구들을 개별로 실행하는 것은 번거로우므로, Makefile에 통합하면 편리합니다.

Makefile
.PHONY: fmt lint test check ci

fmt:
	goimports -w .

lint:
	golangci-lint run ./...

test:
	go test ./...

check: fmt lint test

ci: lint test

check`와 `ci 두 가지 타겟을 나눈 이유가 있습니다. fmt 타겟은 파일을 직접 수정하므로 로컬에서 커밋 전에 실행하기에 편리하지만, CI 환경에서는 파일을 수정하면 안 됩니다. ci 타겟은 `golangci-lint`의 gofmt, goimports 린터가 포맷 위반을 검출만 하고 파일을 수정하지 않으므로, CI에서 안전하게 쓸 수 있습니다.

  • 로컬: make check (포맷 자동 수정 + 린트 + 테스트)

  • CI: make ci (린트 + 테스트, 파일 수정 없음)

`go vet`을 별도 타겟으로 두지 않은 이유는, golangci-lint에 govet 린터가 포함되어 있어서 `make lint`에서 이미 실행되기 때문입니다.

코드 수정 후 커밋하기 전에 `make check`를 실행하는 습관을 들이면, CI에서 실패하는 상황을 줄일 수 있습니다.

6. 참고 자료

(이 글은 개인 라이센스로 구매한 Claude Code의 도움을 받아 작성되었습니다.)

Claude Code Hook으로 quota 전환 자동화하기

Claude Code의 SessionStart hook을 활용하여 여러 quota 간 전환을 자동화하는 방법을 정리합니다.

Claude Code를 여러 quota로 사용하는 경우가 있습니다. 예를 들면 다음과 같습니다.

  • Max plan을 2개를 구독해서 번갈아 가면서 쓰는 경우

  • 같은 이메일 계정에 여러 organization이 연결되어 각각 별도의 quota가 있는 경우

  • Team plan과 Enterprise plan을 동시에 구독하는 경우

이런 상황에서 한쪽 quota가 소진되었을 때 다른 쪽으로 전환해서 쓸 수 있습니다.

문제는 quota A가 소진되어 quota B로 전환한 뒤, quota A가 초기화되는 시점에 다시 돌아오는 걸 잊기 쉽다는 것입니다. Claude Code의 Hook 기능을 활용하면 이 전환을 자동화할 수 있습니다.

구현 목표

  1. Quota A가 소진되면, 초기화 예정 시간을 기록한다.

  2. /logout 후 quota B로 로그인하여 계속 작업한다.

  3. 초기화 시간이 지난 뒤 새 세션을 시작하거나 `/clear`를 하면, 자동으로 quota B의 credentials를 제거하여 quota A로 재로그인을 유도한다.

Claude Code에게 구현 요청

아래와 같은 프롬프트로 Claude Code에게 구현을 요청했습니다.

Claude Code를 quota A, quota B 두 개로 쓰고 있어. Quota A가 소진되면 초기화 예정 시간을 기록하고 quota B로 전환해서 쓰다가, 초기화 시간이 지난 후 새 세션이나 /clear 시 자동으로 quota B를 logout해서 quota A로 복귀하고 싶어. SessionStart hook으로 구현해줘.

Claude Code는 이 요청을 두 개의 스크립트와 `settings.json`의 hook 설정으로 구현했습니다.

생성된 코드 분석

초기화 시간 기록 스크립트

~/.claude/scripts/record-quota-reset.sh 는 quota A의 초기화 예정 시간을 기록하는 스크립트입니다.

record-quota-reset.sh
#!/bin/bash
# Record when quota A resets so the hook can switch back from quota B
# Usage: record-quota-reset.sh "2026-03-29 09:00"
#        record-quota-reset.sh 4h    (4 hours from now)
#        record-quota-reset.sh 30m   (30 minutes from now)

RESET_FILE="$HOME/.claude/quota-switch-reset-time"

if [ -z "$1" ]; then
    echo "Usage: $0 <datetime or duration>"
    echo "  Examples:"
    echo "    $0 '2026-03-29 09:00'   # absolute time"
    echo "    $0 4h                    # 4 hours from now"
    echo "    $0 30m                   # 30 minutes from now"
    exit 1
fi

INPUT="$1"

# Parse duration format (e.g., 4h, 30m)
if [[ "$INPUT" =~ ^([0-9]+)h$ ]]; then
    HOURS="${BASH_REMATCH[1]}"
    RESET_EPOCH=$(date -d "+${HOURS} hours" +%s)
    RESET_DISPLAY=$(date -d "+${HOURS} hours" "+%Y-%m-%d %H:%M:%S")
elif [[ "$INPUT" =~ ^([0-9]+)m$ ]]; then
    MINS="${BASH_REMATCH[1]}"
    RESET_EPOCH=$(date -d "+${MINS} minutes" +%s)
    RESET_DISPLAY=$(date -d "+${MINS} minutes" "+%Y-%m-%d %H:%M:%S")
else
    # Absolute datetime
    RESET_EPOCH=$(date -d "$INPUT" +%s 2>/dev/null)
    if [ $? -ne 0 ]; then
        echo "Error: Cannot parse datetime '$INPUT'"
        exit 1
    fi
    RESET_DISPLAY=$(date -d "$INPUT" "+%Y-%m-%d %H:%M:%S")
fi

echo "$RESET_EPOCH" > "$RESET_FILE"
echo "Quota A reset time recorded: $RESET_DISPLAY"
echo "Saved to: $RESET_FILE"
echo ""
echo "Now run /logout in Claude Code and log in with quota B."
echo "When the reset time arrives, the next session start or /clear will auto-logout quota B."

절대 시간("2026-03-29 09:00")과 상대 시간(4h, 30m) 두 가지 형식을 지원합니다. 내부적으로는 Unix epoch 타임스탬프로 변환하여 ~/.claude/quota-switch-reset-time 파일에 저장합니다.

자동 전환 hook 스크립트

~/.claude/scripts/check-quota-switch.sh 는 SessionStart hook에서 실행되는 스크립트입니다.

check-quota-switch.sh
#!/bin/bash
# SessionStart hook: check if quota A has reset
# If so, remove credentials to force re-login with quota A

RESET_FILE="$HOME/.claude/quota-switch-reset-time"
CRED_FILE="$HOME/.claude/.credentials.json"

# No reset time recorded, nothing to do
if [ ! -f "$RESET_FILE" ]; then
    exit 0
fi

RESET_EPOCH=$(cat "$RESET_FILE")
NOW_EPOCH=$(date +%s)

if [ "$NOW_EPOCH" -ge "$RESET_EPOCH" ]; then
    # Time to switch back to quota A
    RESET_DISPLAY=$(date -d "@$RESET_EPOCH" "+%Y-%m-%d %H:%M:%S")

    # Backup quota B credentials and remove current credentials
    if [ -f "$CRED_FILE" ]; then
        cp "$CRED_FILE" "$HOME/.claude/.credentials.quota-b.bak"
        rm -f "$CRED_FILE"
    fi

    # Remove reset time file
    rm -f "$RESET_FILE"

    # Exit with code 2 to block session and show message
    echo "Quota A has reset (reset time: $RESET_DISPLAY)." >&2
    echo "Quota B credentials have been removed." >&2
    echo "Please restart Claude Code to log in with quota A." >&2
    exit 2
fi

# Not yet time to switch
exit 0

이 스크립트의 핵심 동작은 다음과 같습니다.

  • 기록된 초기화 시간 파일이 없으면 `exit 0`으로 아무 동작 없이 종료합니다. 평소에는 hook이 세션 시작을 방해하지 않습니다.

  • 현재 시간이 초기화 시간 이상이면 전환을 실행합니다.

  • ~/.claude/.credentials.json~/.claude/.credentials.quota-b.bak 으로 백업한 후 삭제합니다. Credentials 파일이 없으면 Claude Code가 다음 시작 시 로그인을 요청합니다.

  • `exit 2`로 세션을 블로킹하고 stderr로 안내 메시지를 출력합니다. Claude Code hook에서 exit code 2는 세션 진행을 차단하고 stderr 메시지를 사용자에게 보여주는 약속된 규칙입니다.

settings.json hook 설정

~/.claude/settings.jsonSessionStart hook을 등록합니다.

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/.claude/scripts/check-quota-switch.sh"
          }
        ]
      },
      {
        "matcher": "clear",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/.claude/scripts/check-quota-switch.sh"
          }
        ]
      }
    ]
  }
}

SessionStart 이벤트는 matcher 로 세션이 시작된 원인을 구분할 수 있습니다. startup 은 새 세션 시작, clear/clear 명령 실행 시 발생합니다. 두 경우 모두 같은 스크립트를 실행하도록 설정했습니다.

사용 방법

1단계: quota A 소진 시 초기화 시간 기록

Quota A가 소진되면 Claude Code에게 초기화 예정 시간을 알려줍니다. Claude Code가 record-quota-reset.sh 스크립트를 실행하여 시간을 기록합니다.

quota A가 다 됐어. 4시간 후에 초기화돼.

Claude Code는 이 요청을 받아 다음과 같이 스크립트를 실행합니다.

~/.claude/scripts/record-quota-reset.sh 4h

물론 터미널에서 직접 스크립트를 실행할 수도 있습니다.

# 절대 시간
~/.claude/scripts/record-quota-reset.sh "2026-03-29 09:00"

# 상대 시간
~/.claude/scripts/record-quota-reset.sh 4h
~/.claude/scripts/record-quota-reset.sh 30m

2단계: quota B로 전환

Claude Code에서 /logout 을 실행한 후 quota B 계정으로 재로그인합니다.

3단계: 자동 복귀

기록한 초기화 시간이 지난 후에 Claude Code를 새로 시작하거나 `/clear`를 실행하면 hook이 동작합니다. Quota B의 credentials가 자동으로 제거되고, Claude Code를 재시작하면 quota A로 로그인할 수 있습니다.

초기화 시간 기록의 자동화 가능성

현재 1단계(초기화 시간 기록)는 사용자가 직접 시간을 알려줘야 합니다. Pro plan이나 Max plan의 경우 quota 소진 시 Claude Code가 초기화 예정 시간을 메시지로 안내하는데, 이 메시지를 hook으로 캡처할 수 있다면 record-quota-reset.sh 실행까지 자동화할 수 있습니다.

이를 확인하기 위해 진단용 hook을 설정하여 quota 소진 시 어떤 이벤트 데이터가 전달되는지 로깅할 수 있습니다.

log-hook-event.sh
#!/bin/bash
# Diagnostic hook: log all event data received via stdin

LOG_FILE="$HOME/.claude/hook-events.log"
INPUT=$(cat)

echo "=== $(date '+%Y-%m-%d %H:%M:%S') ===" >> "$LOG_FILE"
echo "$INPUT" | jq . >> "$LOG_FILE" 2>/dev/null || echo "$INPUT" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"

exit 0

이 스크립트를 Notification, Stop, StopFailure 이벤트에 hook으로 등록합니다.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/.claude/scripts/log-hook-event.sh"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/.claude/scripts/log-hook-event.sh"
          }
        ]
      }
    ],
    "StopFailure": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "/home/user/.claude/scripts/log-hook-event.sh"
          }
        ]
      }
    ]
  }
}

Quota가 소진되면 ~/.claude/hook-events.log 에 이벤트 데이터가 기록됩니다. 이 로그를 Claude Code에게 보여주며 자동화 스크립트 작성을 요청하면 됩니다.

hook-events.log 내용 보여줘. quota 소진 시 자동으로 record-quota-reset.sh가 실행되도록 hook을 만들어줘.

로그에 초기화 시간 정보가 포함되어 있다면 파싱하여 `record-quota-reset.sh`를 자동 실행하는 hook을 만들 수 있습니다. 포함되어 있지 않다면 이 단계의 자동화는 불가능하며, 사용자가 직접 초기화 시간을 입력해야 합니다.

한계

  • credentials 파일 의존: ~/.claude/.credentials.json 파일을 직접 조작하는 방식이므로, Claude Code의 내부 인증 구조가 변경되면 동작하지 않을 수 있습니다.

  • 완전 자동 로그인은 불가: Credentials 제거 후 재시작 시 로그인 화면이 나타나지만, quota A 계정을 자동으로 선택해주지는 않습니다. 사용자가 직접 quota A 계정으로 로그인해야 합니다.

  • 세션 중간 전환은 지원하지 않음: SessionStart hook은 새 세션 시작이나 /clear 시에만 동작합니다. 세션 중간에 초기화 시간이 지나더라도 현재 세션에서는 전환이 일어나지 않습니다. 매 프롬프트마다 체크하는 방식(UserPromptSubmit hook)도 가능하지만, 세션 중간에 credentials가 제거되면 진행 중인 작업과 컨텍스트가 유실되므로 권장하지 않습니다.

macOS에서의 차이점

Claude Code는 실행 환경의 OS를 인식합니다. macOS에서 같은 요청을 하면 date 명령어 부분이 BSD 문법으로 생성됩니다. 주요 차이는 다음과 같습니다.

용도 Linux (GNU date) macOS (BSD date)

상대 시간 (4시간 후)

date -d "+4 hours" +%s

date -v+4H +%s

절대 시간 파싱

date -d "2026-03-29 09:00" +%s

date -j -f "%Y-%m-%d %H:%M" "2026-03-29 09:00" +%s

epoch → 표시 형식

date -d "@$EPOCH" "+%Y-%m-%d %H:%M:%S"

date -r $EPOCH "+%Y-%m-%d %H:%M:%S"

Claude Code의 '/model opusplan’과 showClearContextOnPlanAccept 옵션

Claude Code에서 /model opusplan 설정과 showClearContextOnPlanAccept 옵션을 소개합니다.

/model opusplan

/model opusplan 을 선택하면 Opus로 plan을 세우고 Sonnet으로 실행하는 전략을 쓸 수 있습니다. 복잡한 작업에서 Opus의 추론 능력으로 계획을 세우고, 실행은 비용이 낮은 Sonnet으로 처리하는 방식입니다. 그런데 같은 세션 안에서 모델이 전환되면, 그때까지 쌓인 히스토리 전체가 캐시 미스로 재과금됩니다. 턴이 많이 진행된 후에 전환할수록 삼각수 효과로 누적된 토큰이 크기 때문에 비용 타격이 큽니다.

Tip
토큰 비용의 삼각수 효과와 Prompt Caching에 대한 자세한 설명은 Claude Code 토큰 비용과 프롬프트 캐싱을 참고하세요.

showClearContextOnPlanAccept 설정

세션 중간에 모델이 전환될 때 발생하는 부정적인 효과를 줄이는데 도움이 될 수도 있는 옵션이 있습니다. Claude Code 설정showClearContextOnPlanAccept 옵션이 true 이면, plan 승인 시 컨텍스트를 클리어할지 선택하는 옵션이 나타납니다. 'Yes, clear context(??% used) and by pass permissions' 를 선택하면, 컨텍스트를 클리어하면 planning 단계에서 쌓인 히스토리가 제거된 상태에서 실행이 시작되므로, 모델 전환으로 인한 캐시 무효화 비용을 피할 수 있습니다.

이 선택지가 나오도록 활성화하려면 ~/.claude/settings.json 에 다음과 같이 추가합니다.

{
  "preferences": {
    "showClearContextOnPlanAccept": true
  }
}

showClearContextOnPlanAccept은 기본값 변경 이력

Claude Code의 CHANGELOG에 따르면 showClearContextOnPlanAccept은 아래와 같은 이력을 가지고 있습니다.

  • v2.1.0(2026-01-07) : "Clear context" 옵션이 plan 승인 시 기본 선택으로 처음 도입

  • v2.1.2(2026-01-09) : Shift+Tab 단축키로 이 옵션을 건너뛸 수 있는 기능도 추가

  • v2.1.81(2026-03-20) : 기본값이 false 로 변경

따라서 2026년 3월 말 현 시점에서는 showClearContextOnPlanAccept 속성값을 직접 추가하지 않으면 "clear context" 옵션이 나타나지 않습니다.

Plan 실행 전 컨텍스트 클리어의 트레이드오프

이 옵션은 opusplan이 아닌 경우에도 유용할 때도 있습니다. 같은 모델로 plan mode를 사용하더라도, plan 단계에서 많은 턴이 쌓였다면 컨텍스트를 클리어하고 실행을 시작하면 이후 턴의 입력 토큰 크기를 줄일 수 있습니다.

그런데 컨텍스트 클리어에는 트레이드오프가 있습니다. Plan 파일 자체는 디스크(~/.claude/plans/)에 저장되어 보존되지만, planning 중에 읽은 파일 내용이나 탐색 결과는 모두 사라집니다. 따라서 실행 단계에서 필요한 파일을 plan을 가이드 삼아 다시 읽어야 하며, 이 과정에서 추가 토큰이 소비됩니다. planning 중의 논의 내용 중 plan 텍스트에 반영되지 않은 부분도 유실됩니다.

Plan 실행 전에 컨텍스트 클리어에 대해서 아래와 같은 부정적인 사용자 피드백이 있습니다.

  • #18523 Plan 승인 대화상자의 기본 옵션을 설정할 수 있게 해달라는 요청. 컨텍스트를 유지해야 복잡한 문제에서 더 나은 결정을 내릴 수 있고, auto-compact가 이미 컨텍스트를 관리하므로 강제 클리어가 불필요하다는 의견.

  • #18878 "Clear context" 기본 옵션을 비활성화하거나 순서를 변경할 수 있게 해달라는 요청. plan에 포함되지 않은 대화 맥락이 유실되는 문제를 지적하며, 파괴적인 동작이 기본값이 되어서는 안 된다는 의견.

  • #25734 "Clear context and implement"가 기본 옵션인 것은 파괴적이고 되돌릴 수 없다는 문제 제기. 터미널 스크롤백까지 지워져 복구가 불가능하며, 가장 파괴적인 옵션이 기본값이 되어서는 안 된다는 의견.