ui.render 이벤트를 발생시키며, 해당 이벤트에 대한 훅은 그곳에 그릴 내용을 반환합니다.
다음 맵은 터미널 세션에서 mod가 그릴 수 있는 위치를 보여 줍니다.
개별 prop이나 제한을 찾아보려면 레퍼런스를 참조하십시오.
탭이 있는 창 만들기
이 섹션에서는/hello-tabs 명령을 추가하는 mod를 만들며, 이 명령은 창을 엽니다. 창은 너비가 넓은 전체 화면 터미널에서는 트랜스크립트 옆의 사이드바로, 그 외의 경우에는 프롬프트 위의 테두리가 있는 영역으로 표시됩니다. 이 창에는 두 개의 탭이 표시되며, 두 번째 탭에는 카운터를 1씩 늘리는 버튼이 있습니다. 카운트는 Claude Code를 다시 시작한 후에도 유지됩니다.
완성된 mod는 다음과 같습니다. 녹화 영상은 창을 열고, 두 번째 탭으로 전환한 뒤, 버튼을 몇 번 누르고, 첫 번째 탭으로 돌아갑니다.
1
플러그인 만들기
mod는 매니페스트, 코드를 가리키는
hooks.json, 그리고 코드 파일로 구성된 플러그인입니다. 각 항목에 대한 설명은 mod 만들기를 참조하세요. hello-tabs라는 디렉터리를 만들고 그 안에 .claude-plugin 및 hooks 디렉터리를 만든 다음, 처음 두 파일을 저장합니다.매니페스트를 hello-tabs/.claude-plugin/plugin.json으로 저장합니다.hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json에 진입점을 지정합니다.hello-tabs/hooks/hooks.json
2
코드 작성하기
다음 목록은 코드에 나타나는 순서대로 각 훅이 하는 일을 설명합니다.각 훅은 코드만으로는 드러나지 않는 작업도 수행합니다.
/hello-tabs명령을 추가하고, 이전 세션에서 저장한 카운트를 불러옵니다- 해당 명령을 실행하면 창을 엽니다
- 창의 콘텐츠, 즉 탭 줄과 열린 탭의 본문을 그립니다
tab과 count가 창의 상태를 보관합니다.다음을 hello-tabs/hooks/register.js로 저장합니다.hello-tabs/hooks/register.js
session.start는 세션 간에 유지되는 키-값 저장소인$.store에서 저장된 카운트도 읽습니다.command.run은 Claude Code에 창이 존재한다는 사실만 알립니다. 창을 여는 것만으로는 아무것도 그려지지 않습니다. 그 후 Claude Code가ui.render를 발생시켜 창에 무엇이 들어갈지 묻습니다.ui.render는 다른 박스, 텍스트, 버튼을 담는Box인 요소 트리를 반환하며, 실행될 때마다tab과count로부터 트리를 다시 빌드합니다.
onPress 콜백이 실행되어 변수를 변경하고 redraw를 호출합니다. 그러면 Claude Code가 ui.render 훅을 다시 실행하고, 훅은 새 값으로부터 새 트리를 빌드합니다. 모든 인터랙티브 그리기는 이 렌더 주기를 사용합니다. 콜백이 상태를 변경하면, 훅이 새 상태로부터 다시 렌더링합니다.3
창 열기
셸에서
claude --plugin-dir ./hello-tabs로 Claude Code를 시작합니다. Claude Code 프롬프트에서 /hello-tabs를 실행합니다. 상단에 1: One과 2: Two가 표시된 창이 열립니다. 2를 누른 다음, Add one의 단축키인 a를 몇 번 누릅니다. 카운트가 올라갑니다.4
카운트가 저장되었는지 확인하기
Esc를 눌러 창을 닫은 다음 세션을 종료합니다. 셸에서 동일한
claude --plugin-dir ./hello-tabs 명령으로 Claude Code를 다시 시작하고, Claude Code 프롬프트에서 /hello-tabs를 실행합니다. 카운트가 마지막으로 남겨 둔 값 그대로 유지되어 있습니다.카운트를 지우려면 mod가 $.store.delete('count')를 호출하도록 합니다. 각 종류의 값이 얼마나 오래 유지되는지는 상태 유지하기에서 다룹니다.그릴 위치 선택하기
ui.render 훅은 그리려는 렌더링 지점 하나로 범위를 좁히지 않으면 모든 렌더링 지점에서 실행됩니다. 렌더링 지점을 선택하려면 matcher라고 하는 필터를 on의 두 번째 인수로 전달합니다. { component: 'Pane' }은 pane에서만 훅을 실행합니다. 훅 안에서 e.component는 지점의 이름을, e.surface는 어느 앱이 그리고 있는지를 나타내며, e.props에는 해당 지점 고유의 데이터가 담깁니다. pane의 경우 e.requestId는 pane을 열 때 사용한 id입니다.
pane과 band는 mod가 채우기 전까지 비어 있습니다. 탭을 선택하여 각각이 무엇이며 어떻게 그리는지 확인하십시오.
- Pane
- 프롬프트 위의 band
pane은 너비가 넓은 전체 화면 터미널에서는 트랜스크립트 옆의 사이드바이며, 그 외의 경우에는 프롬프트 위의 테두리가 있는 영역입니다. 여러 pane이 열려 있으면 각 pane에 제목을 표시하는 탭이 생깁니다.pane은 mod가 직접 정한
id로 $.ui.open을 호출할 때 나타납니다. 예: $.ui.open({ id: 'hello-tabs' }). 다른 필드와 pane이 더 넓은 터미널을 기다리는 경우는 적절한 시점에 pane 열기에서 다룹니다.pane에 그리려면 { component: 'Pane' }으로 필터링하고 e.requestId가 해당 id인지 확인합니다.Claude Code가 이미 그리는 요소 변경하기
Claude Code는 메시지, 도구 호출 행, 스피너 등 인터페이스의 대부분을 직접 그립니다. 이러한 각 부분도 렌더링 지점이므로 mod로 스타일을 변경하거나 대체할 수 있습니다. 하나를 변경하려면 다음 표에 있는 이름으로ui.render 훅을 필터링합니다.
Pane, AskUserQuestion, Spinner, UserMessage, AssistantMessage 및 도구 호출 지점은 터미널, Desktop 앱, VS Code 확장에서 작동합니다. 나머지 각 지점이 발생하는 위치는 렌더링 지점 표에 나와 있습니다.
Claude Code가 이미 그리는 지점에서 훅은 세부 사항을 변경하거나, 그리기를 대체하거나, 그대로 둘 수 있습니다. 탭을 선택하여 각 방식을 스피너에 적용한 예를 확인하십시오. 예제는 튜토리얼 mod에서처럼 다른 훅이 세는 calls 변수를 읽습니다.
- 세부 사항 변경
- 그리기 대체
- 그대로 두기
Claude Code의 그리기를 유지하면서 일부만 변경하려면 스피너는 애니메이션과 단어를 유지하고, 단어 뒤에 사용자가 지정한 텍스트가 이어집니다.
props를 변경한 이벤트 복사본을 next에 전달합니다. 다음 훅은 스피너 단어 뒤의 텍스트를 변경합니다.next(e)는 뒤에 실행되는 mod가 자체 트리를 반환하지 않는 한 Claude Code의 그리기에 대한 참조인 { type: 'engine', ref }를 반환합니다. 그 그리기의 내용을 변경하려면 세부 사항 변경 탭에서처럼 prop을 다르게 한 이벤트 복사본을 next에 전달합니다. 참조를 그대로 반환하거나, Box 안에 직접 만든 요소와 나란히 배치할 수 있습니다.
under the spinner가 나타납니다.
권한 프롬프트는 렌더링 지점이 아니므로 mod가 표시 내용을 변경할 수 없습니다. 질문 대화 상자인 AskUserQuestion은 렌더링 지점이므로 mod가 변경할 수 있습니다. 대화 상자용 트리는 참조를 정확히 한 번 포함해야 하며, 직접 만든 요소는 그 위에 있어야 합니다. 그렇지 않으면 Claude Code가 자체 대화 상자를 그립니다.
적절한 시점에 pane 열기
pane은 mod가 열 때만 나타납니다. pane을 여는 방법과 시점에 따라 키보드 포커스를 가져가는지, 얼마만큼의 공간을 요청하는지, 좁은 터미널에서 표시되는지 여부가 결정됩니다. pane을 열려면 직접 정한id로 $.ui.open을 호출합니다. id는 pane의 이름입니다. ui.render 훅이 이 값을 확인하며, pane을 닫을 때도 다시 전달합니다.
id로 $.ui.close를 호출합니다.
$.ui.open은 id 외에 다음 선택적 필드를 받습니다.
VS Code 확장은
rows와 columns를 무시하고 pane 크기를 자체적으로 정합니다.
focus, closeOnEscape, holdToasts는 선택 사항이며 true만 허용합니다. 사용하지 않으려면 생략하십시오. false를 전달하면 ui.open: focus is true or left out과 같은 오류가 발생합니다. 이 중 하나를 조건부로 설정하려면 조건이 충족될 때만 필드를 추가합니다. 다음 호출은 items가 비어 있지 않을 때만 키보드 포커스를 요청합니다.
immediate: true를 추가합니다. 이 설정이 없으면 턴 중에 입력한 명령은 턴이 끝날 때까지 대기합니다.
대화 상자 뒤에 토스트 보류하기
pane이 사용자가 응답한 뒤 떠나는 대화 상자인 경우$.ui.open에 holdToasts: true를 전달하여 사용자가 결정하는 동안 토스트가 나타나지 않도록 합니다. 터미널에서 보류는 해당 pane이 표시되는 동안 유지되며, 그 사이에 발생한 토스트는 보류가 끝날 때까지 대기합니다.
Claude Code는 mod가 $.ui.toast로 발생시키는 토스트뿐 아니라 다른 mod의 토스트와 Claude Code 자체의 짧은 알림도 보류합니다. 계속 열려 있는 pane에서는 사용자가 이러한 알림을 계속 볼 수 있도록 이 필드를 생략하십시오.
pane이 더 넓은 터미널을 기다리는 경우
사용자의 요청 없이 mod가 연 pane은 좁은 터미널에서 나타나지 않으므로 작은 화면을 차지할 수 없습니다. 표시 여부는 pane을 연 주체에 따라 달라집니다.- 사용자가 실행한 명령이나 누른 버튼처럼 사용자의 동작으로 열린 경우, pane은 너비와 관계없이 나타납니다
- 타이머나
turn.start훅처럼 mod가 스스로 동작하여 열린 경우, pane은 너비가 144열 이상인 터미널에서만 나타납니다. 사용자가 해당 pane을 직접 한 번 연 후에는 110열이면 충분합니다.
$.ui.open은 { isPlaced: true }로 resolve됩니다. pane이 대기 중이면 isPlaced는 false이고 reason은 그 이유를 설명하는 문자열입니다. 대기 중인 pane은 사용자가 열거나 터미널 너비를 넓히면 나타납니다. pane을 열지 않고 무언가를 사용할 수 있음을 알리려면 $.ui.toast('Your message')를 호출하여 토스트 알림을 표시합니다.
VS Code 확장에서 그리기
VS Code 확장에서는 확장 버전이 2.1.299 이상이고 기능 플래그를 가져오는 세션인 경우 mod가 그리는 내용이 채팅 패널에 나타납니다. 패널용ui.render 훅에서 e.surface는 vscode입니다. 패널이 어떤 지점과 요소를 그리는지, 어떤 prop을 무시하는지 확인하려면 렌더링 지점 및 요소 표를 참조하십시오.
mod가 터미널뿐 아니라 패널에서도 작동하도록 하려면 다음 차이점을 고려하십시오.
- Pane: 너비가 800픽셀 이상인 패널에서는 pane이 대화 옆에 도킹되며,
e.props.placement는'dock'이고e.viewport.isFullscreen은true입니다. 더 좁은 패널에서는 pane이 프롬프트 상자 위에 그려지고,placement는'inline'이며, pane은 스크롤되기 전까지 12행을 표시합니다. - 토스트와 상태줄: 프롬프트 상자 바로 위에 하나의 스택으로 놓이며, 패널이 좁을 때는 pane도 함께 놓이고, mod마다 상태줄이 하나씩 표시됩니다. 권한 요청이나 대화 상자가 표시되는 동안에는 스택이 숨겨집니다.
- 레이블: 패널은 mod가 그리거나 다시 쓴 내용에 테두리를 두르고 플러그인 이름으로 레이블을 붙입니다. 예: From the token-chart plugin 또는 Edited by the token-chart plugin.
- 키보드: 버튼에는 단축키가 없으므로 사용자는 버튼을 클릭하거나 버튼에 포커스가 있을 때 Enter를 누릅니다.
- 키보드 포커스: 패널은 패널에 포커스가 있고, Claude가 대기 중인 질문 없이 유휴 상태이며, 프롬프트 상자가 비어 있을 때만 pane의
focus: true요청을 허용합니다. - 질문 대화 상자:
AskUserQuestion에서 패널은 자체 질문 카드를 유지하고 그 위에 직접 만든 요소를 그립니다.next에 전달한 prop은 카드를 변경하지 않습니다. - 프롬프트 제안: 패널은 프롬프트 상자의 흐린 제안 텍스트를 Claude 자체 제안용으로 유지하므로
$.prompt.suggest는isShown: false로 resolve되며 아무것도 표시하지 않습니다. - 텍스트 제한: 패널은 상태줄을 200자 또는 두 행에서 자르고, 토스트는 500자에서 자릅니다.
요소로 트리 만들기
ui.render 훅이 반환하는 것은 요소 트리입니다. 요소 트리는 무엇을 그릴지에 대한 설명으로, 서로 중첩된 박스, 텍스트, 컨트롤로 구성됩니다. 그릴 내용을 기술하면 Claude Code가 사용자가 열어 둔 앱, 즉 터미널, Desktop 앱 또는 VS Code 확장 프로그램 중 어디에서든 이를 렌더링합니다.
요소를 가져오려면 const { Box, Text, Button } = $.ui.resolve(e)처럼 훅에서 $.ui.resolve(e)를 호출합니다. 각 요소는 함수입니다. 함수에 prop을 전달하고, 그 안에 들어갈 요소와 문자열은 children에 넣습니다.
탭을 선택하면 가장 일반적인 각 요소와 터미널에서 그려지는 모습을 확인할 수 있습니다.
- Text
- Box
- Input
Text는 문자열을 그리며, bold, color 같은 스타일을 선택적으로 적용할 수 있습니다.
모듈이
.tsx 또는 .jsx 파일이라면 트리를 JSX로 작성할 수 있습니다. 먼저 $.ui.resolve(e)에서 요소를 구조 분해하십시오.
트리에 앱에 없는 요소, 요소가 받지 않는 prop, 또는 자식이 들어갈 수 없는 곳의 자식이 사용되면 Claude Code는 해당 영역을 자체 버전으로 그립니다.
--plugin-dir로 시작한 세션에서는 ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own과 같은 트랜스크립트 줄로 이를 알려 줍니다. 디버그 로그에는 같은 사유와 함께 ui.render (Pane): a hook returned a tree that does not validate로 기록됩니다. 세션에는 그 밖에 아무것도 표시되지 않으므로, 그린 내용이 나타나지 않는다면 해당 줄이나 로그를 확인하십시오.
Desktop 앱과 VS Code의 Link
Desktop 앱과 VS Code 확장 프로그램에서 Link는 href가 다음 요구 사항을 충족하지 않으면 일반 텍스트로 그려집니다.
- 스킴과 호스트:
https:URL 또는http://localhost:3000같은http://localhostURL @사용 불가: 경로나 쿼리에 있는@는%40으로 작성합니다- 표기: 호스트 뒤에
/가 빠진 것을 제외하면new URL(href).href가 반환하는 값과 같아야 합니다. 따라서 대문자 호스트, 공백,https:URL의:443은 허용되지 않습니다.
Client가 실패할 때
터미널에서 Client가 실행하는 파일이 실패하면 my-mod: Client client/spinner.js: boom 같은 흐리게 표시된 줄이 Client의 자리를 대신하며, 그린 내용의 나머지 부분은 그대로 표시됩니다.
mod가 ui.fault를 처리하면 Claude Code는 이후 해당 영역을 다시 그립니다.
색상 셀 그리드 그리기
터미널에서 히트맵, 스파크라인, 게임 보드를 그리려면 셀마다Box를 사용하지 말고 Raster 하나를 그리십시오. Raster는 key, columns와 rows로 지정하는 크기, 그리고 모든 셀을 담은 base64 문자열인 cells를 받습니다. 각 셀은 세 개의 숫자로 구성됩니다. 문자의 코드 포인트, 문자 색상, 배경 색상입니다. 색상은 빨간색의 0xc62828처럼 16진수로 표기한 24비트 RGB 값입니다. 이 범위보다 1 큰 값인 0x01000000은 터미널의 기본 색상을 의미합니다.
터미널 이외의 앱에는 Raster가 없으므로 e.surface를 확인하여 그곳에서는 텍스트를 그리십시오. 다음 창 본문은 3×2 히트맵을 그립니다.
rows 배열이며, cellsOf가 이를 압축된 문자열로 변환합니다. 이 훅은 id가 heat인 창에서만 그리므로, hello-tabs 예시가 창을 여는 것처럼 명령에서 $.ui.open({ id: 'heat' })로 창을 여십시오.
각 문자는 한 셀 너비여야 합니다. 이미 화면에 있는 Raster에 애니메이션을 적용하려면 창의 id를 requestId로, Raster의 key, 동일한 크기, 새 셀을 지정하여 $.ui.blit을 호출하십시오. 이 예시에서는 $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })입니다. 이 호출은 ui.render 훅을 다시 실행하지 않고 해당 요소 하나만 다시 그립니다.
누르기와 입력에 응답하기
사용자가 mod가 그린 버튼을 누르거나, 필드에 입력하거나, 목록에서 항목을 고르면 Claude Code는 해당 컨트롤의 콜백을 호출하며, 이 콜백은 모듈에서 실행됩니다. 각 컨트롤은 자체 콜백을 받습니다.Button:onPress(e)를 받으며, 여기서e.surface는 누르기가 발생한 앱입니다Input:onSubmit(value)와onInput(value)를 받습니다Select:options에 선택지를 담은onSelect(value)를 받습니다.options는 고유한 값을 가진 선택지를 하나 이상 포함하는 목록이며, 예를 들어[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]와 같습니다
key를 통해 컨트롤을 누르거나 컨트롤에 입력하므로 각 컨트롤에 key를 지정합니다. 또한 컨트롤을 사용할 때마다 e.element에 key를 담아 ui.press, ui.input 또는 ui.select가 발생하며, 다른 mod가 이러한 이벤트를 처리할 수 있습니다. 해당 훅은 콜백보다 먼저 실행되므로 사용자가 Input에 입력하는 내용을 볼 수 있고, 이를 변경하거나 콜백 대신 응답할 수 있습니다. mods API에는 다른 mod의 버튼을 누르는 메서드가 없습니다.
키보드 포커스와 단축키
mod는 키보드를 직접 읽지 않습니다. 사용자가 키를 누르면 Claude Code가 그 키가 어느 컨트롤을 위한 것인지 결정하고, 해당 컨트롤의 콜백이 실행됩니다. 밴드의 숫자 단축키를 제외하면 이는 창이나 밴드에 키보드 포커스가 있는 동안에만 일어납니다. 그 외에는 키가 프롬프트로 전달됩니다. VS Code 확장 프로그램에서는 버튼에 단축키가 없습니다. 사용자는 버튼을 클릭하거나 버튼에서 Enter를 누르며, 필드에는 직접 입력합니다.창이 키보드 포커스를 얻는 방법
창은 다음과 같은 경우 키보드 포커스를 얻습니다.- mod가 명령이나 누르기에서
focus: true로 창을 여는 경우 - 사용자가 Ctrl+X를 누른 다음 Tab을 누르는 경우
- 사용자가 창을 클릭하는 경우
focus: true를 허용합니다. 사용자가 입력하는 중에 열리는 창은 사용자의 키 입력을 가져가지 않습니다.
각 키의 동작
다음 표는 창이나 밴드에 키보드 포커스가 있는 동안 각 키가 수행하는 동작을 보여 줍니다.
mod는 Tab이나 화살표 키를 다른 동작에 바인딩할 수 없으므로, 게임은
w, a, s, d로 조작합니다.
단축키와 첫 포커스 설정하기
hotkey: 사용자가 키 하나로Button을 누를 수 있게 하려면hotkey: 'a'처럼 숫자 하나 또는 소문자 하나로 된hotkey를 지정합니다autoFocus: 창이 열릴 때 어느 컨트롤에 포커스를 둘지 선택하려면 해당 컨트롤에autoFocus: true를 추가합니다. VS Code 확장 프로그램에서는autoFocus가 아무런 효과가 없습니다. 컨트롤은 이 prop을 생략한 것처럼 그려지며, 따로 변경할 필요는 없습니다. 이 prop은true만 허용하므로 다른 컨트롤에서는 생략합니다.
터미널에서는 사용자가 무엇을 눌러야 하는지 알 수 있도록 대괄호 버튼의 레이블에 키 이름을 넣거나
plain: true를 사용합니다. 요소 레퍼런스에서 action, 밴드의 숫자 단축키, 하나의 단축키에 연결된 두 버튼 등 Button의 다른 규칙을 확인할 수 있습니다.
입력을 받고 항목마다 행 그리기
많은 창은 텍스트 필드 아래에 목록이 있는 형태입니다. 이 섹션의 예시는 메모 창입니다. 메모를 입력하고 Enter를 눌러 추가하며, 각 메모에는 메모를 삭제하는x 버튼이 있습니다. 메모 두 개를 추가하면 터미널은 창을 다음과 같이 그립니다.
✕는 창을 닫기 위한 Claude Code 자체의 표시입니다.
이 예시는 다음 기법을 사용합니다.
- 입력 받기:
Input은 사용자가 Enter를 누르면 필드의 텍스트로onSubmit(value)를 호출하고, 변경이 있을 때마다onInput(value)를 호출합니다 - 목록 그리기: 데이터를 항목마다 한 행으로 매핑하고, 모든 행의 버튼에 고유한
key를 지정합니다
- 메모 추가: 한 줄을 입력하고 Enter를 누릅니다. 입력한 줄이 새 행으로 나타나고 필드가 비워집니다.
- 메모 삭제: 메모의
x버튼에 포커스가 갈 때까지 Tab을 누른 다음 Enter를 누릅니다.x는 버튼의 레이블이며 단축키가 아니므로, 해당 문자를 입력해도 버튼이 눌리지 않습니다.
hello-tabs와 동일한 렌더링 주기를 따릅니다. 콜백이 notes를 변경하고, redraw를 호출하고, 목록을 $.store에 저장합니다.
제출할 때마다 필드가 비워지는 것은 value prop 때문입니다. value는 필드를 그릴 때 필드에 담기는 텍스트이며, 훅이 필드를 다시 그릴 때까지는 사용자의 입력이 이를 대체합니다. 이 예시는 항상 ''로 필드를 그립니다.
이 예시는 메모를 저장하지만 불러오지는 않습니다. 다음 세션에서 메모를 다시 불러오려면 hello-tabs가 count를 읽는 방식처럼 session.start 훅에서 메모를 읽습니다.
다음 prop이 필드의 줄 Note: Type a note and press Enter ⏎ add를 구성합니다.
콜백이
$.prompt.submit을 호출하지 않는 한, Input을 제출해도 턴이 시작되지 않습니다.
사이트 다시 그리기
그리기는 스냅샷입니다. 즉,ui.render 훅이 마지막으로 실행되었을 때 반환한 내용을 보여 줍니다. 새로운 내용을 표시하려면 훅이 다시 실행되어야 합니다. Claude Code는 일부 변경에 대해서는 훅을 다시 실행하며, 나머지 경우에는 mod가 다시 그리기를 요청합니다.
요청 없이 Claude Code가 다시 그리는 경우
Claude Code는 사이트의 prop이 변경되거나 터미널의 너비가 변경되면ui.render 훅을 다시 실행합니다. 사이트의 Client가 실패하고 mod가 ui.fault를 처리하는 경우, Claude Code는 ui.fault 훅이 반환된 후 훅을 한 번 더 실행하므로 ui.render 훅에서 해당 Client를 제외할 수 있습니다. 타이머에 따라 훅을 실행하지는 않으며, 모듈 안의 변수가 변경되는 시점을 알 수도 없습니다.
데이터가 변경될 때 다시 그리기
자체 데이터가 변경된 후 사이트를 다시 그리려면$.ui.invalidate('ui.render')를 호출합니다. 다음 창은 누른 횟수를 셉니다. 버튼의 콜백은 count를 변경한 다음 다시 그리기를 요청합니다.
hello-tabs 예제는 동일한 호출을 redraw 함수로 감싸고 있습니다.
$.state에 보관하는 값은 이 호출이 필요하지 않습니다. 값을 쓰면 해당 값을 읽는 사이트가 다시 그려지기 때문입니다.
타이머에 따라 다시 그리기
시계, 카운트다운 또는 세션 외부의 값을 최신 상태로 유지하려면 일정에 따라 다시 그립니다. 모듈의session.start 훅에서 타이머를 시작합니다. hello-tabs처럼 모듈에 이미 해당 훅이 있다면 $.clock.every 줄을 추가합니다.
ui.render 훅을 1초에 한 번씩 실행합니다. 타이머는 모듈이 다시 로드되면 중지되며, 모듈의 새 인스턴스가 자체 타이머를 시작합니다.
사이트를 다시 그릴 수 있는 빈도
Claude Code는 사이트의 다시 그리기를 제한하므로, mod는 데이터가 변경될 때마다$.ui.invalidate를 호출해도 됩니다. 각 사이트를 다시 그릴 수 있는 빈도는 제한 표를 참조하세요.
제한보다 빠르게 들어오는 호출은 하나의 다시 그리기로 병합됩니다. 이 다시 그리기는 훅을 한 번 실행하며, 훅은 그 시점의 데이터를 읽으므로 최신 값이 표시되고 그 사이의 값은 표시되지 않습니다. 애니메이션은 제한보다 빠르게 실행될 수 없습니다.
상태 유지
mod가 값을 어디에 보관하는지에 따라 그 값이 유지되는 기간이 결정됩니다. 모듈이 다시 로드될 때까지, 세션이 끝날 때까지, 또는 한 세션에서 다음 세션까지 유지될 수 있습니다. 값이 유지되어야 하는 기간에 따라 선택합니다.$.store.get(key)는 값 또는 undefined로 resolve되며, $.store.set(key, value)는 모든 JSON 값을 받습니다.
$.state에 값 보관하기
$.state는 세션 동안 값을 보관하며, 자동으로 다시 그려 줍니다. 이는 반응형 상태입니다. 값을 읽는 ui.render 훅은 해당 값을 구독하므로, 값을 쓸 때마다 Claude Code가 해당 사이트를 다시 그리며 $.ui.invalidate를 호출할 필요가 없습니다. 또한 $.state에 있는 값은 모듈이 다시 로드되어도 유지되지만, 변수는 그렇지 않습니다.
설정하려면 값을 선언하고, 매니페스트가 해당 선언을 가리키도록 한 다음, 각 값을 정의하고 사용합니다. 예제에서는 hello-tabs의 count를 $.state로 옮깁니다.
값 선언하기
타입 선언 파일에 값을 선언합니다. 바깥쪽 키는 플러그인 이름이며, 그 아래의 각 항목은 값과 그 타입입니다. 다음을hello-tabs/types/index.d.ts로 저장합니다.
hello-tabs/types/index.d.ts
매니페스트가 선언을 가리키도록 하기
claude plugin validate가 해당 파일을 기준으로 코드를 검사할 수 있도록, 매니페스트에 파일 경로를 담은 types 필드를 추가합니다.
hello-tabs/.claude-plugin/plugin.json
값 정의, 읽기, 쓰기
모듈에서 각 값을 기본값과 함께 정의하고, 그리는 동안 읽고, 콜백에서 씁니다.atom은 값의 이름과 기본값을 지정하고, read는 값을 반환하며, update는 값을 씁니다. 이 세 헬퍼가 대신 $.state.get과 $.state.set을 호출합니다.
ui.render 훅이 count를 읽었기 때문에, 버튼이 값을 쓸 때마다 Claude Code가 훅을 다시 실행합니다.
코드에는 다음 규칙이 적용됩니다.
plugin과key를 문자열 리터럴로 작성합니다:claude plugin validate가 소스에서 이 값을 읽습니다- 각
atom호출 결과를const에 보관합니다:count를let으로 선언하면takes a source the scan can read와 함께 검증이 실패합니다 - 모든 값을 타입 선언 파일에 선언합니다: 그렇지 않으면
hello-tabs.count is not declared와 함께 검증이 실패합니다 - 콜백이나 다른 이벤트의 훅에서 씁니다:
ui.render훅은 상태를 읽을 수는 있지만 쓸 수는 없으므로,onPress,onSubmit또는 다른 이벤트의 훅에서 씁니다
$.state를 사용하도록 hello-tabs 변경하기
hello-tabs의 count를 $.state로 옮기려면 이를 사용하는 모든 줄을 변경합니다.
- 모듈 상단:
import줄을 추가하고,let count = 0을atom줄로 바꿉니다 ui.render훅:tabButton앞에read줄을 추가하고,Text에'Count: ' + n을 그립니다- Add one 버튼:
onPress를 여러 세션에서 저장하기에 있는 것으로 바꿉니다. 이는 카운트를 쓸 뿐 아니라 저장도 합니다 session.start훅:saved를 읽는 두 줄을/clear후 저장된 값 다시 로드하기의loadCount호출로 바꿉니다
tab은 여전히 변수이므로 탭 버튼에는 redraw를 유지합니다.
/clear 후 저장된 값 다시 로드하기
mod가 session.start에서 $.store의 저장된 값을 $.state로 복사한다면, /clear, /resume, /branch 후에 다시 복사해야 합니다. 이러한 명령은 모든 $.state 값을 기본값으로 재설정하며, session.start는 다시 실행되지 않습니다. classic.SessionStart는 각 명령 후에 e.source가 clear, resume 또는 fork로 설정된 채 실행되므로, 이 이벤트의 훅에서 값을 다시 복사합니다. 그렇지 않으면 그리기에 기본값이 표시되고, $.state 값을 저장하는 콜백이 저장해 둔 값을 기본값으로 덮어씁니다.
다음 코드는 두 훅 모두에서 count를 로드합니다. 이는 count가 atom이고 update를 import한 $.state 버전의 hello-tabs를 기반으로 합니다. loadCount를 register 위에 두고, 이미 있는 session.start 훅에 loadCount 호출을 추가합니다. classic.SessionStart는 시작 시와 압축 후에도 실행되는데, 이때는 $.state가 재설정되지 않으므로 source 필터를 사용해 훅을 세 가지 재설정으로 제한합니다.
/clear 후 창에 0이 아닌 저장된 카운트가 표시되며, 다음에 Add one을 누르면 저장된 카운트에 더해집니다.
loadCount는 저장된 값으로 $.state의 값을 덮어쓰며, session.start는 모듈이 다시 로드될 때마다 다시 실행됩니다. 스토어가 뒤처지지 않도록 하려면 Add one 버튼처럼 변경될 때마다 저장합니다.
세션 없이 다시 로드를 확인하려면 /clear 후 그리기를 테스트합니다.
여러 세션에서 저장하기
머신에서 mod를 실행하는 모든 세션은 하나의$.store를 공유합니다. get 다음에 set을 실행하는 것은 원자적이지 않습니다. 두 세션이 각각 값을 읽고, 변경하고, 다시 쓰면 경쟁 상태가 발생하며, 두 번째 쓰기가 첫 번째 쓰기를 대체합니다.
이러한 가능성을 줄이려면 다음을 따릅니다.
- 각 항목에 고유한 키를 부여합니다:
set은 자신의 키만 변경하므로, 서로 다른 키에 쓰는 세션은 서로를 덮어쓰지 않습니다 - 쓰기 직전에 다시 읽습니다: 여러 세션이 변경하는 값의 경우, 콜백에서 키를
get하고session.start에서 로드한 복사본이 아니라 그 값으로 새 값을 만듭니다. 다른 세션의 쓰기가get과set사이에 발생하면 여전히 손실됩니다.
다음 단계
- 이벤트에 반응하기: 도구 호출과 턴으로 드로잉에 데이터를 공급합니다
- mods API 사용하기: 타이머와 모델 호출로 드로잉에 데이터를 공급합니다
- 드로잉 테스트하기: 테스트에서 버튼을 눌러 보고, 여러 사용 환경에서 확인합니다
- 렌더링 지점 및 요소: 각 지점의 prop과 각 요소의 prop을 설명합니다