mod 만들기
설명을 바탕으로 Claude가 Claude Code mod를 작성하도록 하거나, 도구 호출 수를 세고 명령을 추가하는 mod를 직접 작성합니다. 다시 로드하고 검증하는 과정을 알아봅니다.
mod는 훅 모듈이라고 하는 진입 파일을 갖춘 Claude Code 플러그인입니다. 훅 모듈은 이벤트가 발생할 때 Claude Code가 그 함수들을 호출하는 JavaScript 또는 TypeScript 파일입니다. mod를 만드는 방법은 다음과 같습니다.
- Claude에게 작성 요청하기: Claude Code 세션에서 원하는 내용을 설명합니다
- 직접 작성하기: 튜토리얼을 따라 mod의 코드가 어떻게 동작하는지 알아봅니다. Claude Code가
.js및.ts파일을 직접 로드하므로 Node.js, 번들러 또는 빌드 단계가 필요하지 않습니다.
mod가 적합한 도구인지 아직 결정하지 않았다면 먼저 개요의 비교를 읽어 보십시오.
mod를 사용하려면 Claude Code v2.1.287 이상이 필요합니다. 셸에서 claude --version을 실행하여 확인합니다. 현재 환경에서 mod를 로드할 수 있는지 확인하려면 mod를 로드할 수 있는지 확인하기를 참조하십시오.
Claude에게 mod 요청하기
대화형 Claude Code 세션에서 원하는 mod를 설명하면 Claude가 작성합니다. Claude는 plugin-authoring이라는 내장 스킬을 기반으로 작업하며, 이 스킬은 mod를 작성할 위치, 현재 버전에서 사용할 수 있는 이벤트와 메서드, mod가 로드되는 방식을 Claude에게 알려 줍니다. mod를 요청하면 Claude가 스킬을 로드할 수 있으며, Claude Code 프롬프트에서 /plugin-authoring을 실행하여 직접 로드할 수도 있습니다.
mod는 승인한 후에 실행됩니다. 단, Claude가 작성한 mod를 로드할 수 없는 세션은 예외입니다.
mod 설명하기
원하는 mod를 자유롭게 요청합니다. 예를 들어 make a mod that shows the current git branch above the prompt와 같이 요청할 수 있습니다. Claude는 세션의 mod 폴더 안에 mod 전용 디렉터리를 만들어 mod를 작성합니다. 세션의 mod 폴더는 ~/.claude/dev-mods/ 뒤에 세션 ID가 붙은 경로입니다. mod의 전체 경로는 ~/.claude/dev-mods/3f2a9c1e-5b7d-4e8a-9c21-6d0f4b8a7e13/git-branch/와 같은 형태입니다.
default 및 acceptEdits 권한 모드에서는 ~/.claude가 보호된 경로이므로, Claude가 mod의 각 파일을 생성하기 전에 Claude Code가 확인을 요청합니다. 표시되는 각 파일을 승인하십시오.
mod 승인하기
Claude가 첫 번째 파일을 저장하면 Claude Code가 해당 세션에서 핫 리로딩을 활성화할지 묻습니다. 핫 리로딩은 이 세션에서 Claude가 작성한 mod를 실행하고 이후의 각 변경 사항을 반영합니다.
다음 중 하나를 선택합니다.
- Enable for this session: 세션의 mod 폴더에 있는 mod가 턴이 끝날 때 로드되고, mod를 변경하는 각 턴이 끝날 때마다 다시 로드됩니다. 이 선택은 세션을 재개한 후를 포함하여 세션 동안 유지됩니다.
- Not now: 지금은 아무것도 로드되지 않습니다. 파일은 Claude가 작성한 위치에 그대로 남아 있으며, 해당 세션이 다음에 시작될 때 mod가 로드됩니다. mod가 로드되지 않도록 하려면 해당 디렉터리를 삭제하십시오.
mod가 로드되었는지 확인하기
Claude Code 프롬프트에서 /plugin을 실행하고 Installed 탭이 선택될 때까지 Tab 키를 누릅니다. 이 탭에 mod가 표시되며, 여기에서 mod를 끌 수 있습니다.
mod 사용해 보기
요청한 기능을 사용해 봅니다. 예시 프롬프트의 경우 현재 브랜치 이름이 프롬프트 상자 위에 표시됩니다. mod가 원하는 대로 동작하지 않으면 변경할 내용을 Claude에게 알려 주십시오. mod는 파일을 변경하는 각 턴이 끝날 때마다 다시 로드되므로, Claude가 작업을 마치는 즉시 변경 사항을 확인할 수 있습니다.
다른 세션에서 mod 사용하기
Claude가 작성한 mod는 해당 mod를 만든 세션에서만 로드되며, Claude Code는 세션의 mod 폴더가 cleanupPeriodDays보다 오래되면 삭제합니다. mod를 보관하려면 해당 디렉터리를 mod 폴더에서 ~/mods/git-branch와 같은 별도 위치로 복사하십시오. 그런 다음 로드 방법을 선택합니다.
- 직접 시작하는 세션에서: 셸에서
claude --plugin-dir ~/mods/git-branch를 실행합니다 - 다른 사람을 위해: 다른 사람이 설치할 수 있도록 마켓플레이스에 추가합니다
Claude가 작성한 mod를 로드할 수 없는 세션
Claude가 작성한 mod는 mod 실행이 허용된 신뢰할 수 있는 워크스페이스에서 사용자가 승인한 후에만 로드됩니다. 다음 세션에서는 로드되지 않습니다.
- 승인할 사람이 없는 경우:
claude -p실행이나dontAsk모드처럼 세션에서 프롬프트를 표시할 수 없는 경우 - 워크스페이스를 신뢰하지 않는 경우: 해당 디렉터리에 대한 신뢰 프롬프트를 수락하지 않은 경우
- mod가 비활성화된 경우:
--safe-mode또는--bare로 시작했거나,disableAllHooks를 설정했거나, 조직의 관리형 설정에서 차단한 경우
직접 mod 작성하기
이 튜토리얼에서는 Claude가 수행하는 도구 호출 횟수를 세고, Claude가 작업하는 동안 스피너 옆에 그 횟수를 표시하며, 횟수를 출력하는 /tally 명령을 추가하는 first-mod라는 mod를 만듭니다. 그런 다음 Claude Code가 mod 옆에 작성하는 타입 선언을 읽고 claude plugin validate를 실행합니다. 이 두 가지를 통해 사용 중인 버전이 제공하는 이벤트와 메서드, 그리고 Claude Code가 코드에서 읽어 들이는 내용을 확인할 수 있습니다.
다음 녹화 영상은 완성된 mod를 보여 줍니다. 스피너가 도구 호출 횟수를 세고, /tally가 횟수를 출력하며, 세션이 실행되는 동안 코드를 수정하면 바로 적용됩니다.
세 개의 파일을 작성합니다.
first-mod/
├── .claude-plugin/
│ └── plugin.json
└── hooks/
├── hooks.json
└── register.js
plugin.json: 플러그인의 매니페스트hooks.json: 코드 파일을 가리킴register.js: 훅 모듈이라고 부르는 사용자의 코드
플러그인 디렉터리 만들기
파일을 담을 두 개의 디렉터리를 만듭니다.
mkdir -p first-mod/.claude-plugin first-mod/hooks
New-Item -ItemType Directory -Force first-mod\.claude-plugin, first-mod\hooks
매니페스트 작성하기
mod는 플러그인이며, mod에는 매니페스트가 필요합니다. 이 mod의 매니페스트에는 특별한 필드가 없습니다. 다음 내용을 first-mod/.claude-plugin/plugin.json으로 저장합니다.
{
"name": "first-mod",
"version": "0.1.0",
"description": "Counts Claude's tool calls, shows the count beside the spinner, and adds a /tally command",
"author": { "name": "Your Name" }
}
Claude Code에 코드 위치 알려 주기
Claude Code는 플러그인을 로드할 때 플러그인의 hooks/hooks.json을 읽습니다. 이 파일의 modules 키는 코드의 경로를 지정하며, 이 키가 있어야 플러그인이 mod가 됩니다. hooks.json을 기준으로 한 상대 경로 하나를 나열합니다. 여기서는 다음 단계에서 작성할 register.js를 가리킵니다.
다음 내용을 first-mod/hooks/hooks.json으로 저장합니다.
{
"description": "The first-mod hooks module",
"modules": ["./register.js"]
}
코드 작성하기
이 파일은 훅 모듈이라고 부르는 mod의 코드입니다. mod가 로드되면 Claude Code는 파일이 내보내는 register 함수를 호출하고 on이라는 함수를 전달합니다. on을 호출할 때마다 지정한 이벤트에 대한 이벤트 핸들러, 즉 훅이 등록됩니다.
다음 내용을 first-mod/hooks/register.js로 저장합니다.
// The count, shared by the hooks below
let calls = 0
// Claude Code calls this once when the mod loads
export function register(on) {
// Runs when the session starts, before your first prompt
on('session.start', async ($, e, next) => {
// Add the /tally command
await $.command.register({
name: 'tally',
description: 'Show how many tool calls Claude has made',
})
// Let the session start as usual
return next(e)
})
// Runs each time Claude is about to use a tool
on('tool.call', async ($, e, next) => {
calls += 1
// Ask Claude Code to draw the interface again, so the new count shows
$.ui.invalidate('ui.render')
// Let the tool run as usual
return next(e)
})
// Runs when you type /tally, and only then, because of the matcher
on('command.run', { command: 'tally' }, async () => {
// The text to print in the transcript
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}
이 파일은 calls에 횟수를 저장하고 네 개의 훅을 등록합니다.
- **
session.start**는 세션이 시작될 때 첫 프롬프트 전에 실행되며, mod가 다시 로드될 때마다 다시 실행됩니다. Claude Code에/tally명령을 추가합니다. - **
tool.call**은 Claude가 도구를 사용하려고 할 때마다 실행됩니다.calls에 1을 더하고 Claude Code에 인터페이스를 다시 그리도록 요청합니다. - **
command.run**은/tally를 입력할 때 실행됩니다. 출력할 텍스트를 반환합니다. - **
ui.render**는 Claude Code가 스피너를 그릴 때마다 실행됩니다. 스피너의 단어 뒤에 횟수를 추가합니다.
예제 mod의 작동 방식에서 각 훅이 받는 세 개의 인수와 각 훅이 반환하는 값을 설명합니다.
mod 로드하기
--plugin-dir 플래그를 사용하여 Claude Code를 시작합니다. 이 플래그는 플러그인 디렉터리를 설치하지 않고 한 세션 동안 로드합니다.
claude --plugin-dir ./first-mod
mod 사용해 보기
list the files here and read the README처럼 여러 번의 도구 호출이 필요한 작업을 Claude에게 요청합니다. Claude가 작업하는 동안 스피너의 단어 뒤에 Thinking · tool calls: 2…처럼 증가하는 횟수가 표시됩니다. Claude가 작업을 마치면 /tally를 입력하고 Enter 키를 누릅니다. 트랜스크립트에 실제 횟수와 함께 first-mod: Claude has made 2 tool calls since this mod loaded가 표시됩니다. Claude Code는 명령의 텍스트 앞에 플러그인 이름을 붙입니다.
대화형 세션 없이 명령을 확인하려면 비대화형 모드에서 실행합니다.
claude -p "/tally" --plugin-dir ./first-mod
first-mod: Claude has made 0 tool calls since this mod loaded
명령 목록에 /tally가 없다면 모듈이 로드되지 않은 것입니다. mod가 아무 동작도 하지 않는 원인 찾기를 참조하세요.
세션 실행 중에 코드 변경하기
세션을 열어 둔 상태로 register.js의 ui.render 훅에서 ' · tool calls: '를 ' · tools used: '로 변경하고 저장합니다. 강조 표시된 줄이 변경되는 줄입니다.
// Runs each time Claude Code draws the spinner
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Keep Claude Code's spinner, with the count added after its word
return next({ ...e, props: { ...e.props, suffix: ' · tools used: ' + calls + '…' } })
})
트랜스크립트에 first-mod가 다시 로드되었다는 줄과 함께 해당 훅 목록이 표시되고, 다음 스피너에는 Thinking · tools used: 1…처럼 새 텍스트가 사용됩니다.
예제 mod의 작동 방식
on에 전달하는 각 함수는 이벤트 핸들러인 훅입니다. Claude Code는 모든 훅에 동일한 세 개의 인수를 전달합니다.
- mods API(
$): mod가 외부에 접근하기 위해 호출할 수 있는 모든 메서드로,$.ui,$.command와 같은 네임스페이스로 구성됩니다 - 이벤트(
e): 도구 호출의 이름과 인수 등 이벤트의 입력을 일반 데이터로 담고 있습니다 - 다음 핸들러(
next): 이벤트를 다른 mod로, 이어서 Claude Code 자체의 동작으로 전달하고 그 결과를 반환하는 함수입니다
first-mod의 훅은 다음과 같은 방식으로 이벤트를 처리합니다.
- 관찰:
session.start훅은 명령을 등록하고,tool.call훅은 호출 횟수를 세고 다시 그리기를 요청합니다. 두 훅 모두next(e)를 반환하므로 세션이 시작되고 도구가 평소대로 실행됩니다. - 응답:
command.run훅은 자체 결과를 반환하며next를 호출하지 않습니다.on의 두 번째 인수인{ command: 'tally' }는 matcher라고 부르는 필터이므로, 이 훅은/tally에 대해서만 실행됩니다. - 재작성:
ui.render훅은suffix에 횟수를 담은e의 복사본으로next를 호출하므로, Claude Code는 평소의 스피너를 그리되 단어 뒤에 사용자의 텍스트를 표시합니다
Claude Code는 --plugin-dir로 로드된 디렉터리를 감시하며, 그 안의 파일이 변경되면 훅 모듈을 핫 리로드합니다. 다시 로드할 때마다 register가 다시 실행되므로 calls가 0으로 초기화되고 /tally는 처음부터 다시 횟수를 셉니다. 다시 로드해도 값을 유지하려면 상태 유지하기를 참조하세요.
mod 계속 작업하기
mod가 로드되면 Claude에게 mod 변경을 요청하고, 사용 중인 버전의 타입 정의에 맞춰 코드를 검사하고, Claude Code가 mod에서 찾은 이벤트와 호출을 나열하고, mod를 테스트할 수 있습니다.
Claude로 mod 변경하기
이미 있는 mod를 변경하려면 --plugin-dir이 mod의 디렉터리를 가리키도록 하여 세션을 시작합니다. 그러면 Claude가 작성한 내용이 같은 세션에서 로드됩니다.
claude --plugin-dir ./first-mod
그런 다음 변경 사항을 요청합니다. 예를 들어 add a /tally-reset command to this mod that sets the tally back to zero와 같이 요청할 수 있습니다. Claude는 훅 모듈을 편집하고, claude plugin validate를 실행하고, 보고된 문제를 수정합니다. --plugin-dir로 로드한 디렉터리는 보호된 경로이므로, default 및 acceptEdits 모드에서는 Claude가 mod를 편집할 때마다 승인을 요청받습니다. 다른 권한 모드에서의 결과는 보호된 경로 표에서 확인할 수 있습니다.
Claude가 턴 중에 저장한 파일은 턴이 끝날 때 다시 로드되므로, Claude가 작업을 마치는 즉시 /tally-reset을 사용해 볼 수 있습니다.
사용 중인 버전의 타입 정의 가져오기
Claude Code는 --plugin-dir에 전달한 디렉터리의 mod나 Claude가 작성한 mod를 로드하거나 다시 로드할 때마다 .d.ts로 끝나는 TypeScript 선언 파일을 mod 디렉터리 안의 .claude-plugin/types/에 작성합니다. 이 파일은 실행 중인 Claude Code 버전의 정확한 이벤트, mods API 메서드, 요소를 기술하므로 편집기에서 훅을 자동 완성하고 타입 검사할 수 있습니다. 선언을 온라인에서 살펴보려면 Claude Code 저장소의 mods/types/claude-code.d.ts를 읽어 보세요. 이 파일의 첫 줄에는 파일을 작성한 버전이 명시되어 있습니다. 디렉터리에는 다음 파일이 있습니다.
| 경로 | 선언 내용 |
|---|---|
claude-code/index.d.ts |
모든 이벤트와 그 입력 및 결과, 모든 mods API 네임스페이스와 메서드, 각 사용 환경에서 그릴 수 있는 요소 |
claude-code-tools/index.d.ts |
기본 제공 도구의 입력과 결과. e.tool === 'Bash'를 검사하면 e의 타입이 좁혀집니다 |
claude-code-mcp/index.d.ts |
mod의 파일을 마지막으로 저장했을 때 연결되어 있던 MCP 도구의 입력 |
플러그인 이름을 딴 디렉터리의 index.d.ts |
해당 플러그인이 mods API에 추가하는 내용. plugin.json의 dependencies에 나열된 플러그인마다 디렉터리가 하나씩 있습니다. |
tsconfig.json |
훅 모듈에 맞는 컴파일러 옵션 |
mod에 자체 tsconfig.json이 없으면 Claude Code는 생성된 파일을 확장하는 tsconfig.json을 mod의 루트에 추가하므로, 추가 설정 없이 편집기와 tsc -p ./first-mod로 mod를 타입 검사할 수 있습니다.
이벤트와 메서드는 릴리스마다 바뀔 수 있으므로, 이 페이지를 포함한 어떤 페이지와 내용이 다르다면 이 파일을 신뢰하세요.
claude-code/index.d.ts는 사용 중인 빌드에 대한 가장 완전한 참조 자료로, 모든 mods API 메서드에 대한 설명과 예제를 포함합니다. 항목을 찾으려면 'tool.call'처럼 이름으로 파일을 검색하세요.
Claude Code가 mod에서 읽는 내용 확인하기
코드를 실행하거나 세션을 시작하지 않고 Claude Code가 보는 방식 그대로 mod를 확인하려면 claude plugin validate를 사용합니다. 이 명령은 매니페스트를 검사하고, Claude Code가 mod를 로드할 때 실행하는 것과 동일한 정적 분석을 훅 모듈의 소스에 대해 실행합니다. 셸에서 mod의 디렉터리를 대상으로 실행합니다.
claude plugin validate ./first-mod
first-mod의 경우 출력에 다음 줄이 포함됩니다.
❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
❯ ./register.js calls: $.command.register, $.ui.invalidate
✔ Validation passed
hooks: 줄에는 모듈이 훅하는 이벤트가 중괄호 안의 필터와 함께 나열됩니다. calls: 줄에는 모듈이 호출하는 모든 mods API 메서드가 나열됩니다. 환경 변수를 읽거나 설정하는 모듈에는 env reads: 및 env writes: 줄도 표시되고, $.state를 사용하는 모듈에는 state reads: 및 state writes: 줄이 표시됩니다.
처리하려던 이벤트가 첫 번째 줄에 없다면 Claude Code도 해당 훅을 호출하지 않습니다. 일반적인 원인은 이벤트 이름의 오타이며, 이 명령은 이를 "tool.calls" is not an event와 같은 오류로 보고합니다.
정적 분석이 모든 훅과 호출을 찾을 수 있도록 다음 규칙을 따르세요.
- 각 mods API 호출은
$.store.get('notes')처럼$, 네임스페이스, 메서드 순으로 전체를 작성합니다. 같은 파일의 최상위에 선언된 함수에는$를 전달할 수 있으며,loadNotes라는 함수의 경우calls:줄에$.store.get (via loadNotes)로 표시됩니다. 메서드, 훅 안에서 정의된 함수, 또는 다른 파일에서 가져온 함수에$를 전달하면 검증에 실패합니다.$.state가 사용하는read및update함수는$를 받을 수 있는 가져온 함수입니다.$나 그 네임스페이스를 변수에 할당하거나, 구조 분해하거나, 계산된 이름으로 인덱싱하지 마세요.const ui = $.ui는$.ui is used as a value오류로 실패합니다. - 각
on호출의 이벤트 이름은'tool.call'처럼 문자열 리터럴로 작성합니다. 변수나 이름 목록에 대한 반복문을 사용하면the event name passed to on() is not a string literal오류로 실패합니다. register안에서on이라는 이름의 변수나 매개변수를 다시 선언하지 마세요. 검증이"on" is declared again (shadowed)오류로 실패합니다.- 플러그인 디렉터리 안의 파일만 상대 경로로 가져옵니다. 허용되는 유일한 bare import는 타입과 몇 가지 헬퍼를 위한
claude-code입니다. import { name } from './file.js'처럼 파일 상단에import선언을 사용합니다. 동적import()는a dynamic import(); a hooks module imports its own files with an import declaration오류로 실패합니다.- 모든 파일은
require가 아닌import를 사용하는 ES 모듈로 작성합니다. Claude Code가 로드하는 파일 확장자는 참조에 나열되어 있습니다.
mod 테스트하기
mod에 대한 자동화된 테스트를 작성하고 셸에서 claude plugin test로 실행할 수 있으며, 세션, 로그인, 네트워크가 필요하지 않습니다. 테스트는 훅이 처리하는 이벤트를 발생시키고 훅이 수행한 작업을 검사합니다.
다음 테스트는 두 개의 도구 호출을 발생시키고, /tally를 실행하고, 응답이 두 호출을 모두 세는지 확인합니다. first-mod/tests/first-mod.test.ts로 저장합니다.
import { expect, test } from 'claude-code/testing'
test('/tally reports the tool calls the mod has seen', async ($, on) => {
// Answer each tool call in Claude Code's place, so no tool runs
on('tool.call', () => ({ result: 'ok' }))
// Fire two tool calls, which the mod's tool.call hook counts
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Run /tally and check the text its hook returns
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})
셸의 first-mod 디렉터리에서 테스트를 실행합니다.
claude plugin test
출력에는 각 테스트의 이름과 통과 여부가 표시되며, 소요 시간은 실행할 때마다 달라집니다.
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [22.87ms]
1 pass
0 fail
Ran 1 test across 1 file. [0.19s]
모델 호출이나 스토어를 스텁으로 대체하는 방법과 타이머 및 그리기를 테스트하는 방법은 mod 테스트하기에서 다룹니다.
mod 공유하기
mod는 플러그인이므로 매니페스트에서 버전을 관리하며, 다른 사람들은 /plugin 명령으로 이를 설치하고 업데이트합니다. 공유 방법은 대상에 따라 다릅니다.
- 소수의 사람: 플러그인 디렉터리 또는 해당 디렉터리의
.zip파일을 전달하십시오. 마켓플레이스 없이 플러그인 공유하기를 참조하십시오 - 팀: 플러그인마다 하나의 디렉터리를 두는 비공개 저장소와 같은 자체 마켓플레이스에 등록하십시오. 저장소에서 작업하는 모든 사람을 위해 해당 마켓플레이스를 추가하려면 저장소 설정에 등록하십시오
- 조직 전체: 관리자가 관리형 설정을 통해 조직의 mod를 설치할 수 있습니다
- 모든 사람: 마켓플레이스 저장소를 공개하거나 Anthropic 디렉터리에 플러그인을 제출하십시오
그 전에 플러그인의 name을 확인하십시오. claude-로 시작하는 이름처럼 Anthropic 자체 플러그인 이름으로 보이는 이름은 claude plugin validate 검사를 통과하지 못합니다. 이벤트와 메서드는 릴리스마다 변경될 수 있으므로, 테스트에 사용한 Claude Code 버전은 README에 명시하십시오.
설치된 사본이 아닌 디렉터리를 대상으로 --plugin-dir를 사용하여 계속 개발하십시오. Claude Code는 설치된 플러그인을 버전별로 캐시하므로, 버전을 올리고 다시 설치하기 전까지는 수정 사항이 설치된 사본에 반영되지 않습니다.
다음 단계
- 인터페이스에 그리기: 창을 열고, 프롬프트 위에 그리고, 버튼과 텍스트 필드를 추가합니다
- 이벤트에 반응하기: 도구 호출, 프롬프트, 턴에 훅을 연결합니다
- mods API 사용하기: 명령과 도구를 추가하고, 모델을 호출하고, 타이머로 작업을 실행합니다
- mod 테스트하기: Claude Code가 응답할 내용을 스텁으로 대체하고, 타이머와 그리기를 테스트합니다
- mod 문제 해결하기: mod가 아무 동작도 하지 않는 이유와 디버그 로그를 다룹니다
- 기본 제공 mod의 소스 읽기: 각각 훅 모듈과 테스트를 포함한 완전한 플러그인입니다