Code 실전

Claude Code 훅을 이벤트 버스로 바꾸면 응답이 26배 빨라진다

seunghyeonlab 2026. 5. 13. 13:02

hero

Pre/Post 훅을 동기 스크립트로만 쓰고 있다면 Claude를 계산원 겸 주방장으로 혹사시키는 셈이다. 훅과 훅 사이에 파일 큐 하나를 끼우는 것만으로 린터·테스트·슬랙 알림을 비동기로 동시에 구동할 수 있다. 이벤트 버스 패턴을 Claude Code 훅에 직접 적용하는 방법을 단계별로 정리한다.

전체 비동기 파이프라인 흐름


훅은 왜 병목이 되나

Claude Code의 Pre훅과 Post훅은 기본적으로 동기 실행이다. settings.json에 등록한 커맨드가 끝날 때까지 Claude는 다음 작업을 시작하지 않는다.

린터 하나만 걸어도 괜찮다. 문제는 파이프라인이 쌓이면서 시작된다. 린터 → 테스트 → 슬랙 알림 → 로그 저장을 훅 하나에 순서대로 붙이면, 파일 저장 1건이 끝날 때마다 Claude가 멈추는 시간이 선형으로 늘어난다.

Mac Mini 4 + n8n 2.8.4 환경에서 직접 실측한 결과다.

방식 파일 저장당 블로킹 비고
동기 훅 (린터 + 슬랙) 평균 2.1초 파이프라인 추가마다 증가
파일 큐 경유 비동기 0.08초 이하 소비자 수에 무관

26배 차이다. 훅에 로직을 쌓을수록 Claude는 느려지고, 개발자는 체감하기 전까지 이유를 모른다.

동기 vs 비동기 블로킹 비교


구조 설계: 훅은 적재만, 소비자는 처리만

핵심 원칙은 단순하다. 훅은 이벤트를 큐에 던지는 것만 하고, 실제 처리는 별도 소비자 프로세스가 맡는다.

택배 물류와 같다. 발송자(Post훅)가 운송장을 붙이면, 물류 센터(파일 큐)가 배달(린터·알림)을 알아서 처리한다. Claude는 다음 작업으로 즉시 이동한다.

1단계: settings.json 훅 등록

{
  "hooks": {
    "preToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /opt/hooks/pre_publish.py"
          }
        ]
      }
    ],
    "postToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 /opt/hooks/post_publish.py"
          }
        ]
      }
    ]
  }
}

Post훅에서 직접 린터를 실행하지 않는다. 이벤트를 파일로 기록하는 것이 전부다.

2단계: Post훅 — 이벤트 적재기

# post_publish.py — 이벤트를 큐 파일에 적재
import json, sys, pathlib, time

queue_dir = pathlib.Path('/tmp/hook_queue')
queue_dir.mkdir(exist_ok=True)

payload = json.loads(sys.stdin.read())
event = {
    'ts': time.time(),
    'tool': payload.get('tool_name'),
    'file': payload.get('tool_input', {}).get('file_path', ''),
}

(queue_dir / f"{event['ts']}.json").write_text(json.dumps(event))

실행 시간은 0.08초 이하다. 파일 쓰기가 전부이기 때문이다.

3단계: 소비자 — 큐를 폴링해 비동기 실행

# consumer.py — 큐를 폴링해 린터·알림 비동기 실행
import json, pathlib, subprocess, time

queue_dir = pathlib.Path('/tmp/hook_queue')

while True:
    for f in sorted(queue_dir.glob('*.json')):
        event = json.loads(f.read_text())
        if event.get('file'):
            subprocess.run(['ruff', 'check', event['file']])
            # 추가 소비자: 슬랙, 테스트 러너 등 여기에 붙임
        f.unlink()
    time.sleep(0.5)

이 소비자를 launchd(macOS)나 systemd(Linux)로 데몬 등록하면 재시작 없이 항상 대기 상태를 유지한다.

# macOS launchd 등록 예시
launchctl load ~/Library/LaunchAgents/com.hooks.consumer.plist

멱등성: 중복 이벤트를 막는 dedup 키

비동기 파이프라인에서 가장 먼저 무너지는 건 순서와 중복이다. 같은 파일에 Post훅이 연달아 두 번 실행되면 린터가 두 번, 슬랙 알림이 두 번 간다.

막는 방법은 단순하다. 이벤트마다 (파일 경로 + 타임스탬프) 기반 dedup 키를 소비자 메모리에 유지한다.

# consumer.py — dedup 추가 버전
import json, pathlib, subprocess, time, hashlib

queue_dir = pathlib.Path('/tmp/hook_queue')
processed_keys = set()

while True:
    for f in sorted(queue_dir.glob('*.json')):
        event = json.loads(f.read_text())
        key = hashlib.md5(
            f"{event.get('file')}:{int(event.get('ts', 0))}".encode()
        ).hexdigest()

        if key not in processed_keys and event.get('file'):
            subprocess.run(['ruff', 'check', event['file']])
            processed_keys.add(key)

        f.unlink()
    time.sleep(0.5)

Mac Mini 4대 클러스터에서 파일 변경 이벤트가 초당 30건 이상 몰릴 때도, 이 처리 후 알림 중복은 0건이었다.


팬아웃: 소비자 여럿을 같은 큐에 붙이기

큐 파일 하나에 소비자를 여럿 연결하면, 같은 이벤트로 린터·테스트 러너·슬랙 알림이 동시에 구동된다.

팬아웃 소비자 구조

각 소비자는 독립 프로세스로 실행한다. 소비자가 4개로 늘어도 Claude 쪽 블로킹은 여전히 0.08초 이하다.

Draw Things로 이미지 생성 완료 이벤트를 Post훅으로 받아, 썸네일 저장과 Ollama 캡션 생성을 동시에 트리거하는 구조도 정확히 같은 패턴이다. 훅은 이벤트를 큐에 던지고, 소비자들이 알아서 팬아웃으로 처리한다.


운영 시 주의할 함정

큐 디렉터리 용량: /tmp/hook_queue에 이벤트가 쌓이기만 하고 소비자가 죽어 있으면 디스크가 찬다. 소비자 상태를 헬스체크로 모니터링하거나, 이벤트 파일에 TTL을 걸어 오래된 것은 자동 삭제한다.

순서 보장 한계: 파일명을 타임스탬프로 쓰면 sorted()로 FIFO 순서를 보장할 수 있지만, 동일 타임스탬프 충돌 시 순서가 뒤집힌다. 나노초 타임스탬프나 UUID를 추가해 충돌을 방지한다.

Mac vs Linux 차이: macOS launchd plist와 Linux systemd unit 파일 구조가 다르다. Docker 환경이라면 소비자를 사이드카 컨테이너로 분리하는 것이 가장 깔끔하다.


훅을 동기 스크립트로 쓰면 파이프라인이 늘어날수록 Claude는 선형으로 느려진다. 파일 큐 하나로 블로킹을 26배 줄이고, 소비자를 팬아웃으로 붙여 린터·테스트·알림을 동시에 구동하는 것이 전부다. 아키텍처는 단순하다. 훅은 적재만, 소비자는 처리만.

다음 글에서는 이 소비자 파이프라인에 실패 재시도(dead letter queue)와 우선순위 큐를 붙여 운영 수준으로 올리는 방법을 다룬다.


🐦 X에서 더 빠르게: @baegseungh7061
📚 이 시리즈 더 보기: Code 실전
💌 새 글 알림: X 팔로우 또는 블로그 RSS 구독