Claude Code의 모드에서 화면 어디에 무엇을 표시할 수 있는지 확인하기 [모드 입문 ③]
요약
본 글은 Claude Code의 'mod' 기능을 심층적으로 다루며, settings 훅과 플러그인과의 역할 분담을 비교합니다. 특히 모드(mod)를 사용하면 화면에 직접 요소를 표시하거나 기존 UI 부품을 수정하는 등, settings 훅으로는 불가능한 고급 상호작용이 가능함을 실습을 통해 보여줍니다.
핵심 포인트
- 모드는 Claude Code 내부에서 호출되는 JS/TS 함수로, UI 조작 및 개입이 핵심입니다.
- settings 훅은 외부 셸 명령어 실행에 주로 사용되며, 모드와 역할 구분이 명확합니다.
- 모드를 수정하면 재시작 없이 저장만으로 로드가 가능하여 개발 편의성이 높습니다.
- 화면 표시(배너, 페인 등)나 기존 UI 요소 수정을 통해 사용자 경험을 확장할 수 있습니다.
서론
Claude Code의 모드를 통해 무엇을 만들 수 있는지 전체적인 그림을 파악하기 위해, 기초부터 순서대로 직접 만져보며 확인하는 시리즈의 세 번째 글입니다.
첫 번째 글에서는 settings.json의 훅(hook)에 대해, 두 번째 글에서는 플러그인이라는 컨테이너를 확인했습니다.
이대로 모드(mod)로 넘어가면 '훅으로도 할 수 있는 것을, 모드로 다시 작성한 것'이라는 글이 될 것 같았습니다.
그래서 이번에는 먼저 공식 문서를 통해 settings 훅, 플러그인, 모드의 역할 분담을 정리하고, 차이가 가장 크게 나타나는 주제로 직접 실습해 보겠습니다.
- 동일하게 '도구 호출 횟수 세기'를 settings 훅과 모드 양쪽에서 만들어 비교하기
- 모드만 할 수 있는 '화면에 표시하기'에 대해, 네 가지 종류의 부품이 화면 어디에 나타나는지 확인하기
| 용어 | 의미 |
|---|---|
| settings 훅 | settings.json에 작성하는 훅. 정해진 타이밍에 Claude Code 외부에서 셸 명령어 등을 실행함 (첫 번째 글에서 다룬 것) |
| mod | 플러그인 형태로 작성하는 JavaScript/TypeScript 함수. Claude Code 내부에서 호출되어 화면에 그리거나 처리에 개입할 수 있음 |
바쁜 사람을 위한 요약
확인한 질문과 답변.
| 질문 | 답변 |
|---|---|
| 동일하게 '도구 호출 횟수 세기'를 만들면 무엇이 다른가 | 모드는 현재 세션의 횟수를 스피너 옆에 그 자리에서 표시할 수 있었다. settings 훅은 파일에 한 줄을 추가하는 것만으로, 전체 세션 분량이 하나의 파일에 섞인다 |
| 모드를 수정하면 다시 시작해야 하는가 | --plugin-dir로 로드된 모드는 저장하기만 하면 재시작 없이 읽어들여졌다 |
| 모드의 화면 부품은 화면 어디에 나오는가 | 상태 바(status line) = 입력창 아래, 토스트(toast) = 오른쪽 상단에 몇 초간, 배너(banner) = 입력창 위, 페인(pane) = 대화의 오른쪽 프레임 |
settings 훅・모드・플러그인의 역할 분담 (공식 문서에서 발췌).
| settings 훅 | mod | 플러그인 |
|---|---|---|
| 정체 | 이벤트 발생 시 Claude Code가 외부에서 실행하는 셸 명령어・HTTP 요청・프롬프트 | Claude Code 내부에서 호출되는 JavaScript/TypeScript 함수 |
| ... | まとめて配りたい / modを入れたい |
참고: https://code.claude.com/docs/en/plugins/mods/overview 「Compare mods, settings hooks, skills, and MCP servers」
settings 훅에서는 불가능하고, 모드만 할 수 있는 것.
- 화면에 그리기 (대화 옆의 페인, 입력창 위의 배너 등)
- Claude Code가 원래 그리고 있던 부품을 수정하기 (도구 호출 줄, 스피너, Claude가 질문하는 다이얼로그 등)
- 도구 호출이나 요청에 개입하기 (보류하고 질문하기, 도구를 실행하지 않고 답변하기, 다른 모델로 보내기)
- Claude의 턴을 사용하지 않고 그 자리에서 작동하기
/명령어를 추가하기 - 함수끼리 변수를 공유하기 - 입력한 프롬프트 문면을 Claude가 읽기 전에 대체하기 (settings 훅의
UserPromptSubmit은 맥락을 추가할 수만 있고, 프롬프트 자체는 대체할 수 없음)
참고: https://code.claude.com/docs/en/plugins/mods/overview 「What a mod can do」
참고: https://code.claude.com/docs/en/hooks 「Decision control」
mod에서 on(...)에 지정할 수 있는 이벤트 목록 (2.1.288의 타입 정의 기준).
| 분류 | 이벤트 |
|---|---|
| 툴 (Tool) | tool.call (도구를 실행하기 직전. 중지/직접 답변/인수 수정 가능) / tool.check (실행해도 되는지 결정할 때) / tool.describe (도구 설명문을 구성할 때) |
| 화면 (Screen) | ui.render (컴포넌트를 그리기 직전) / ui.resolve (사용 가능한 컴포넌트 목록을 결정할 때) / ui.press (직접 그린 버튼이 눌렸을 때) / ui.input (입력란이 변경/제출되었을 때) / ui.select (선택지가 선택되었을 때) / ui.scroll (페인이나 바가 스크롤되기 직전) / ui.focus (페인이나 바 내에서 포커스가 움직이기 직전) / ui.message (직접 그린 컴포넌트로부터의 메시지) |
| 프롬프트 (Prompt) | prompt.submit (제출 후/턴 시작 전. 문구 수정 가능) / prompt.edit (입력란을 편집했을 때) / prompt.fill (입력란에 초안을 넣을 때) / prompt.suggest (입력란의 흐릿한 후보 문구를 낼 때) / prompt.compose (시스템 프롬프트를 구성할 때) / prompt.section (시스템 프롬프트의 절별) / prompt.context (첫 메시지에 붙는 맥락을 계산할 때) / prompt.attachment (Claude Code가 직접 삽입하는 리마인더 등별) |
| 명령어/설정 (Command/Config) | command.run (슬래시 명령어를 실행하기 직전) / command.describe (명령어 목록을 보여줄 때) / config.set (/config 줄이 바뀔 직전) / config.describe (/config 줄을 목록으로 보여줄 때) |
| 턴 (Turn) | turn.start (모델의 턴 시작) / turn.step (모델에게 요청을 보낼 직전) / turn.complete (턴 종료) |
| 세션 (Session) | session.start (시작 시 및 모드 재로드 시) / session.end (종료 시) / session.append (대화에 한 줄 남길 때마다) / session.compact (대화를 압축하기 직전) / session.receive (외부에서 메시지가 도착했을 때) / session.send (다른 에이전트나 세션으로 보낼 직전) / session.attach ・session.detach (원격 화면 연결/분리) / session.measure (턴별 사용량 측정) |
| 에이전트/스킬 (Agent/Skill) | agent.offer (에이전트 종류를 모델에게 보여줄 때) / agent.spawn (서브 에이전트를 시작하기 직전) / skill.prompt (스킬의 지시문을 전개할 때) |
| 기타 (Other) | attribution.text (커밋이나 PR의 서명 문구를 만들 때) / telemetry.log ・telemetry.mark (측정 기록) / plugin.register (모드가 로드되기 직전) / engine.create ($를 구성할 때) / process.spawn (자식 프로세스의 출력) |
| settings 훅 이벤트 | classic.<이벤트명> (classic.Stop 등)으로, settings 훅과 동일한 입력을 모드로 받을 수 있음 |
참고: Claude Code 2.1.288에 포함된 타입 정의 claude-code.d.ts의 EngineEventOf (plugin-authoring 스킬을 로드하면 출력됨)
사전 준비
환경은 Claude Code 2.1.289(도중에 2.1.288에서 자동 업데이트됨), macOS.
1번째, 2번째에서 사용한 실험용 폴더 mods-lab을 그대로 사용할 것.
# 실험용 폴더로 이동
cd mods-lab
mods-lab/.claude/settings.json에는 2번째에서 처음에 작성했던
에 한 줄 추가하는 '훅(hook)'이 들어 있습니다.
이번에는 이것을 settings 훅 측의 '카운트 메커니즘'으로 그대로 사용합니다.
{
"hooks": {
"PreToolUse": [
...
mod의 드로잉(rendering)은 터미널에서 실행한 claude와 데스크톱 앱의 Code 탭에서만 나옵니다.
VS Code 확장 프로그램의 채팅 화면이나 claude -p에서는, 훅은 작동하지만 드로잉이 나오지 않습니다.
이 문서의 조작은 모두 터미널의 claude에서 진행합니다.
참고: https://code.claude.com/docs/en/plugins/mods/overview 「Where mods run」
직접 해보기
'도구 호출 횟수 카운트'를 settings 훅과 mod로 만들어 비교하기
mod 만들기
공식 문서에 나와 있는 최소한의 mod를 그대로 만듭니다.
도구 호출 횟수를 세어, Claude가 작업 중으로 보여주는 스피너 옆에 횟수를 표시하는 mod입니다.
# mod 폴더와 명찰 보관소 .claude-plugin, 훅 보관소 hooks를 한 번에 만듭니다.
mkdir -p count-mod/.claude-plugin count-mod/hooks
# 명찰 plugin.json을 작성합니다.
...
Validating plugin manifest: /Users/ando/projects/my_agent/mods-lab/count-mod/.claude-plugin/plugin.json
⚠ Found 1 warning:
❯ author: No author information provided. Consider adding author details for plugin attribution
...
- 컨테이너는 두 번째 플러그인과 같습니다(
.claude-plugin/plugin.json
+hooks/hooks.json) - 차이점은,
hooks.json이 `
が出た -
Unraveling
은 Claude Code가 작업 중에 매번 바꿔서 보여주는 문구이며, (7s · ↓ 76 tokens)는 원래 표시되어 있던 경과 시간과 토큰 수입니다. 이어서 Read로 파일을 읽었을 때,
tool calls: 2…
으로 늘어난 것은 on('tool.call', ...)에 도구 필터링(tool filtering)을 작성하지 않았기 때문에, Bash와 Read 모두 계산되기 때문입니다.
settings 훅 측의 기록 확인하기
같은 회차의 대화가 settings 훅 측에는 어떻게 남아있는지 살펴봅니다.
다른 터미널에서 줄 수를 세어봅니다.
# mods-lab으로 이동하여 hook-input.jsonl의 줄 수 세기
cd /Users/ando/projects/my_agent/mods-lab && wc -l hook-input.jsonl
10 hook-input.jsonl
이전까지 4줄이었는데 이번에 1줄이 더해져 총 5줄이 되었어야 합니다.
각 줄이 어떤 세션의 것인지 확인합니다.
# hook-input.jsonl의 각 줄에서 세션 ID의 처음 8글자와 실행된 명령어만 한 줄씩 표시하기
jq -c '{s: .session_id[0:8], cmd: .tool_input.command}' hook-input.jsonl
{"s":"65381673","cmd":"ls"}
{"s":"d7d4a1c9","cmd":"ls"}
{"s":"5f4c34c7","cmd":"ls"}
...
- 이번 세션(
674a285e)에 대한 것은 마지막 1줄만 있고, Read에 대한 기록은 없습니다. settings 훅의matcher가Bash이기 때문에, Read에서는 실행되지 않습니다. 나머지 5줄은 같은 프롬프트로 미리 시작했던 다른 세션 2개의 기록입니다.
| 용어 | 의미 |
|---|---|
| session_id | 세션(claude를 한 번 시작해서 끝날 때까지)마다 부여되는 ID |
저장하고 재시작하지 않고 표시 변경하기
첫 번째 settings 훅은 수정할 때마다 claude를 다시 시작하여 반영했습니다.
mod는 어떨까요?
IDE에서 count-mod/hooks/register.js의 ' · tool calls: '을 ' · 툴 호출 수: '로 변경하고 저장합니다.
# 수정된 줄을 표시하여 확인하기
grep -n suffix count-mod/hooks/register.js
12: return next({ ...e, props: { ...e.props, suffix: ' · 툴 호출 수: ' + calls + '…' } })
닫지 않은 이전 세션에서
부품 | 작성 방법
|---|---|
| 상태 라인 (ステータスライン) | $.ui.status(문자열) 호출 |
| 토스트 (トースト) | $.ui.toast(문자열) 호출 |
| 배너 (帯) | ui.render의 { component: 'AbovePrompt' }로 부품 반환 |
| 페인 (ペイン) | $.ui.open({ id, title })로 열고, ui.render의 { component: 'Pane', requestId: id }로 부품 반환 |
하나의 mod에 하나씩 추가하며 저장하고, ls를 요청하여 나타난 위치를 확인한다.
상태 라인만 만드는 모드
배너와 페인은 화면 부품을 <Text>…</Text> 같은 태그로 작성(JSX)한다.
JSX는 .js에서는 읽히지 않으므로, 처음부터 파일명을 register.jsx로 지정해야 한다.
# mod의 폴더와 명찰 보관소 .claude-plugin, 훅 보관소 hooks를 모아서 만든다
mkdir -p screen-mod/.claude-plugin screen-mod/hooks
# 명찰 plugin.json을 작성한다
...
Validating plugin manifest: /Users/ando/projects/my_agent/mods-lab/screen-mod/.claude-plugin/plugin.json
⚠ Found 1 warning:
❯ author: No author information provided. Consider adding author details for plugin attribution
...
| 용어 | 의미 |
|---|---|
| JSX | JavaScript 안에 <Text>…</Text> 같은 태그로 화면 부품을 작성하는 방식. .jsx 파일에서 사용 가능 |
상태 라인
# screen-mod를 이 세션에만 로드하여 Claude Code를 시작한다
claude --plugin-dir ./screen-mod
'ls를 실행해 줘'라고 요청한다.
이후에도 같은 세션을 유지하며 부품을 추가해 나간다.
* Crunched for 7s · done 10:10
────────────────────────
❯ (입력란)
...
- 입력란 아래, Claude Code 자체의
auto mode on줄 위에 나타난 - 줄의 시작 부분에 있는⚠ screen-mod:는 직접 작성한 것이 아니다. 플러그인 이름이 자동으로 붙는다 - 답변이 끝나도 사라지지 않는다. 다음에$.ui.status를 호출할 때까지 남아있다.
| 용어 | 의미 |
|---|---|
| 상태 라인 | 입력란 아래에 남는, 상태를 나타내는 한 줄. 하나의 mod당 한 줄 |
토스트
IDE에서 register.jsx의 $.ui.status(...) 줄 바로 아래에 한 줄을 추가하여 저장한다.
$.ui.toast('토스트: ' + e.tool + ' 를 사용')
같은 세션에서 '다시 ls 해 줘'라고 요청한다.
- 화면 오른쪽 상단에 '토스트: Bash를 사용'이라는 작은 상자가 나타났다가 몇 초 만에 사라졌다
- 상태 라인과 달리 남아있지 않다. 그 자리에서 한마디 알려주는 용도
| 용어 | 의미 |
|---|---|
| 토스트 | 화면 구석에 몇 초만 나타나고 저절로 사라지는 작은 알림 |
배너 (帯)
tool.call 블록 뒤에, 배너를 그리는 훅을 추가하여 저장한다.
let calls = 0
export function register(on) {
on('tool.call', async ($, e, next) => {
...
같은 세션에서 '다시 ls 해 줘'라고 요청한다.
* Worked for 5s · done 10:13
배너: 도구 호출 횟수 1
────────────────────────
...
- 입력란 위의 테두리 바로 위에 나타났다
- 상태 라인은 입력란 아래, 배너는 입력란 위에와 같이, 입력란을 사이에 두고 나뉜다
- 배너는 직접 조립한 부품이 그대로 나온다.
⚠ screen-mod:는 붙지 않는다.
| 용어 | 의미 |
|---|---|
| 帯(AbovePrompt) | 입력란 바로 위에 mod가 그리는 가로로 긴 줄 |
$.ui.resolve(e) | 해당 화면에서 사용할 수 있는 부품(Text ・Box ・Button 등)을 받는 함수 |
페인 (Pane)
페인은 '열기'와 '그리기' 두 가지로 한 세트입니다.
이번에는 직접 만든 /pane 명령어로 엽니다.
register.jsx 파일을 통째로 다음 내용으로 저장합니다.
let calls = 0
export function register(on) {
on('session.start', async ($, e, next) => {
...
같은 세션에서 /pane를 입력합니다.
● screen-mod: reloaded (4 hooks: session.start, command.run, tool.call, ui.render)
❯ /pane
└ screen-mod: 페인을 열었습니다
- 대화의 오른쪽에 세로선으로 구분된 위에서 아래까지의 창이 나타났습니다(오른쪽 위에 닫기
×). - 내용은페인: 도구 횟수 0입니다 (다시 읽으면 0부터 다시 계산). -/pane는 Claude를 거치지 않고, mod의 함수가 그 자리에서 답변했습니다. 대화에는screen-mod: 페인을 열었습니다만 남습니다.
4가지 부품의 위치를 정리하면 다음과 같습니다.
┌────────────────────────────────┬────────────┐ ← 토스트(오른쪽 위에 몇 초 동안)
│ 대화 │ 페인 │
│ ❯ /pane │ 페인: │
...
| 용어 | 의미 |
|---|---|
| 페인 (Pane) | 대화 옆에 mod가 그리는 창. $.ui.open으로 열고, ui.render의 Pane으로 내용을 그립니다 |
session.start | 세션 시작 시와, mod가 다시 읽힐 때 발생하는 이벤트. 여기서 명령어를 등록합니다 |
command.run | 등록한 명령어가 입력되었을 때 발생하는 이벤트 |
페인의 숫자가 늘지 않음
계속
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기