> ## Documentation Index
> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 모드로 인터페이스에 그리기

> Claude Code 모드에서 창, 프롬프트 위의 밴드, 버튼, 텍스트 필드를 그리고, 누름과 입력을 처리하며, 다시 그릴 때와 세션 간에 상태를 유지합니다.

모드는 Claude Code에서 자신의 인터페이스를 그릴 수 있고 Claude Code가 이미 그리는 인터페이스의 일부를 변경할 수 있습니다. 모드가 그릴 수 있는 각 위치를 [렌더 사이트](/docs/ko/plugins/mods/reference#render-sites)라고 하며, 창, 프롬프트 위의 밴드, 또는 스피너 같은 것들이 있습니다. Claude Code는 렌더 사이트를 그리려고 할 때마다 [`ui.render`](/docs/ko/plugins/mods/reference#interface) 이벤트를 발생시키고, 해당 이벤트에 대한 훅이 그곳에 그릴 내용을 반환합니다.

이 지도는 터미널 세션에서 모드가 그릴 수 있는 위치를 보여줍니다:

<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5fda26b6609c62b68c6f9e528c1590ea" className="dark:hidden" alt="Claude Code 터미널 세션의 지도. 모드는 오른쪽에 사이드바로 창을 추가하고, 대화 기록의 오른쪽 위에 토스트를 추가하고, 대화 기록에 로그 줄을 추가하고, 프롬프트 위에 밴드를 추가하고, 프롬프트 아래에 상태 줄을 추가할 수 있습니다. 모드는 메시지, 도구 호출 행, 스피너를 다시 그릴 수 있습니다. 프롬프트는 Claude Code 자신의 것입니다." width="600" height="336" data-path="images/mods-screen-map.svg" />

<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map-dark.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5b4161581a1bd2c0450b0c8b57bc1225" className="hidden dark:block" alt="Claude Code 터미널 세션의 지도. 모드는 오른쪽에 사이드바로 창을 추가하고, 대화 기록의 오른쪽 위에 토스트를 추가하고, 대화 기록에 로그 줄을 추가하고, 프롬프트 위에 밴드를 추가하고, 프롬프트 아래에 상태 줄을 추가할 수 있습니다. 모드는 메시지, 도구 호출 행, 스피너를 다시 그릴 수 있습니다. 프롬프트는 Claude Code 자신의 것입니다." width="600" height="336" data-path="images/mods-screen-map-dark.svg" />

더 좁은 터미널에서는 창이 대화 기록 옆이 아니라 프롬프트 위에 있습니다.

여기서 시작하기 전에 [첫 번째 모드](/docs/ko/plugins/mods/create)를 만드세요. 두 개의 탭과 카운터가 있는 창을 만드는 작업 예제로 시작한 다음, 변경하려는 각 부분에 대한 섹션을 읽으세요.

<Note>
  한 가지 속성이나 제한을 찾으려면 [참조](/docs/ko/plugins/mods/reference#render-sites)를 보세요.
</Note>

<h2 id="build-a-pane-with-tabs">
  탭이 있는 창 만들기
</h2>

이 섹션에서는 `/hello-tabs` 명령을 추가하는 모드를 만들고, 이 명령은 창을 엽니다. 창은 넓은 전체 화면 터미널에서는 대화 기록 옆의 사이드바이거나, 그렇지 않으면 프롬프트 위의 프레임된 영역입니다. 이 창은 두 개의 탭을 표시하고, 두 번째 탭에는 카운터에 1을 더하는 버튼이 있습니다. Claude Code를 다시 시작한 후에도 카운트는 여전히 있습니다.

완성된 모드는 다음과 같습니다. 녹화는 창을 열고, 두 번째 탭으로 전환하고, 버튼을 몇 번 누르고, 첫 번째 탭으로 돌아갑니다:

<Frame>
  <video autoPlay muted loop playsInline controls className="w-full dark:hidden" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-light.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=49d520094d87b5b44bfe50fa49677f06" aria-label="/hello-tabs 명령이 Claude Code 프롬프트에 입력되고 프레임된 창이 위에 열리며, 맨 위에 '1: One'과 '2: Two'가 있고 '이것은 첫 번째 탭입니다.'라는 텍스트가 있습니다. 두 번째 탭은 'Count: 1' 옆에 'Add one' 버튼을 표시하고, 카운트는 3으로 올라갑니다. 창은 그 다음 첫 번째 탭으로 돌아갑니다." data-path="images/mods-hello-tabs-light.mp4" />

  <video autoPlay muted loop playsInline controls className="w-full hidden dark:block" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-dark.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=ff7a14d713d6e5d3b0000efa8522ea4b" aria-label="/hello-tabs 명령이 Claude Code 프롬프트에 입력되고 프레임된 창이 위에 열리며, 맨 위에 '1: One'과 '2: Two'가 있고 '이것은 첫 번째 탭입니다.'라는 텍스트가 있습니다. 두 번째 탭은 'Count: 1' 옆에 'Add one' 버튼을 표시하고, 카운트는 3으로 올라갑니다. 창은 그 다음 첫 번째 탭으로 돌아갑니다." data-path="images/mods-hello-tabs-dark.mp4" />
</Frame>

Claude Code에는 기본 제공 탭 요소가 없으므로 탭은 행의 두 버튼입니다. 모드는 어느 것이 활성인지 추적하고 그 탭의 내용을 행 아래에 그립니다.

<Steps>
  <Step title="플러그인 만들기">
    모드는 매니페스트, 코드를 가리키는 `hooks.json`, 그리고 코드 파일이 있는 플러그인입니다. [모드 만들기](/docs/ko/plugins/mods/create#write-a-mod-yourself)는 각각을 설명합니다. `hello-tabs`라는 디렉토리를 만들고 그 안에 `.claude-plugin`과 `hooks` 디렉토리를 만든 다음 처음 두 파일을 저장하세요.

    매니페스트를 `hello-tabs/.claude-plugin/plugin.json`으로 저장하세요:

    ```json hello-tabs/.claude-plugin/plugin.json theme={null}
    {
      "name": "hello-tabs",
      "version": "0.1.0",
      "description": "Opens a pane with two tabs and a counter",
      "author": { "name": "Your Name" }
    }
    ```

    `hello-tabs/hooks/hooks.json`에서 진입점의 이름을 지정하세요:

    ```json hello-tabs/hooks/hooks.json theme={null}
    {
      "modules": ["./register.js"]
    }
    ```
  </Step>

  <Step title="코드 작성">
    코드는 각 훅에서 하나씩 세 가지 작업을 수행합니다:

    * `/hello-tabs` 명령 추가
    * 해당 명령을 실행할 때 창 열기
    * 창의 내용 그리기: 탭 행과 열린 탭의 본문

    두 개의 모듈 수준 변수인 `tab`과 `count`는 창의 상태를 유지합니다.

    이를 `hello-tabs/hooks/register.js`로 저장하세요:

    ```javascript hello-tabs/hooks/register.js theme={null}
    // 창의 id, 창을 열고 그릴 때 인식하는 데 사용됨
    const PANE = 'hello-tabs'

    // 창이 표시하는 것: 어느 탭이 열려 있는지, 그리고 카운터의 값
    let tab = 'one'
    let count = 0

    export function register(on) {
      // 첫 프롬프트 전에 실행되고, 다시 로드 후에도 실행됨
      on('session.start', async ($, e, next) => {
        await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
        // 이전 세션이 저장한 카운트를 로드합니다 (있는 경우)
        const saved = await $.store.get('count')
        if (typeof saved === 'number') count = saved
        return next(e)
      })

      // /hello-tabs를 입력할 때 실행됨
      on('command.run', { command: 'hello-tabs' }, async ($) => {
        // 창을 열고, 키보드를 주고, Esc로 닫을 수 있게 함
        await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
        // 대화 기록에 아무것도 인쇄하지 않음
        return {}
      })

      // Claude Code가 창을 그릴 때마다 실행됨
      on('ui.render', { component: 'Pane' }, async ($, e, next) => {
        // 다른 모드의 창은 그대로 두기
        if (e.requestId !== PANE) return next(e)
        // 이 앱이 그릴 수 있는 요소 가져오기
        const { Box, Text, Button } = $.ui.resolve(e)
        // Claude Code가 이 훅을 다시 실행하도록 요청
        const redraw = () => $.ui.invalidate('ui.render')

        // 한 탭: 누르면 해당 탭으로 전환하는 버튼
        const tabButton = (name, label, hotkey) =>
          Button({
            key: 'tab-' + name,
            label,
            hotkey,
            plain: true,
            // 열려 있지 않은 탭을 어둡게 함
            dimColor: tab !== name,
            onPress: () => {
              tab = name
              redraw()
            },
          })

        // 어느 탭이 열려 있는지에 따라 탭 아래에 가는 것
        const body =
          tab === 'one'
            ? [Text({ children: ['This is the first tab.'] })]
            : [
                Box({
                  flexDirection: 'row',
                  columnGap: 2,
                  children: [
                    Button({
                      key: 'more',
                      label: 'Add one',
                      hotkey: 'a',
                      onPress: async () => {
                        count += 1
                        redraw()
                        // 카운트를 저장하여 다시 시작 후에도 있도록 함
                        await $.store.set('count', count)
                      },
                    }),
                    Text({ children: ['Count: ' + count] }),
                  ],
                }),
              ]

        // 전체 창: 탭 행, 빈 줄, 그 다음 본문
        return Box({
          flexDirection: 'column',
          children: [
            Box({
              flexDirection: 'row',
              columnGap: 3,
              children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
            }),
            Text({ children: [' '] }),
            ...body,
          ],
        })
      })
    }
    ```

    각 훅은 코드가 명확하게 하지 않는 것도 수행합니다:

    * \*\*[`session.start`](/docs/ko/plugins/mods/reference#session)\*\*는 또한 [`$.store`](#keep-state)에서 저장된 카운트를 읽습니다. 이는 세션 간에 지속되는 키-값 저장소입니다.
    * \*\*[`command.run`](/docs/ko/plugins/mods/api#add-a-command)\*\*은 Claude Code에 창이 존재한다고만 알립니다. 창을 열어도 아무것도 그려지지 않습니다: Claude Code는 그 다음 `ui.render`를 발생시켜 그곳에 무엇을 넣을지 묻습니다.
    * \*\*`ui.render`\*\*는 요소 트리를 반환합니다. 이는 다른 상자, 텍스트, 버튼을 보유하는 `Box`이며, 실행될 때마다 `tab`과 `count`에서 다시 빌드합니다.

    버튼을 누르면 `onPress` 콜백이 실행되고, 이는 변수를 변경하고 `redraw`를 호출합니다. Claude Code는 그 다음 `ui.render` 훅을 다시 실행하고, 훅은 새로운 값에서 새로운 트리를 빌드합니다. 모든 대화형 그리기는 그 렌더 사이클을 사용합니다: 콜백이 상태를 변경하고, 훅이 새로운 상태에서 다시 렌더링합니다.
  </Step>

  <Step title="창 열기">
    셸에서 `claude --plugin-dir ./hello-tabs`로 Claude Code를 시작하세요. Claude Code 프롬프트에서 `/hello-tabs`를 실행하세요. 맨 위에 `1: One`과 `2: Two`가 있는 창이 열립니다. `2`를 누르고, 그 다음 **Add one**의 핫키인 `a`를 몇 번 누르세요. 카운트가 올라갑니다.
  </Step>

  <Step title="카운트가 저장되었는지 확인">
    Esc를 눌러 창을 닫고 세션을 종료하세요. 셸에서 같은 `claude --plugin-dir ./hello-tabs` 명령으로 Claude Code를 다시 시작하고, Claude Code 프롬프트에서 `/hello-tabs`를 실행하세요. 카운트는 남겨둔 곳에 있습니다.

    카운트를 지우려면 모드가 `$.store.delete('count')`를 호출하도록 하세요. [상태 유지](#keep-state)는 각 종류의 값이 얼마나 오래 지속되는지를 다룹니다.
  </Step>
</Steps>

<h2 id="pick-where-to-draw">
  그리기 위치 선택
</h2>

`ui.render` 훅은 원하는 위치로 좁히지 않는 한 모든 렌더 사이트에서 실행됩니다. 렌더 사이트를 선택하려면 [매처](/docs/ko/plugins/mods/events#filter-which-events-a-hook-handles)라고 불리는 필터를 `on`의 두 번째 인수로 전달합니다. `{ component: 'Pane' }`은 훅을 창에서만 실행합니다. 훅에서 `e.component`는 사이트의 이름을 지정하고, `e.surface`는 어떤 앱이 그리고 있는지 나타내며, `e.props`는 사이트 자체의 데이터를 보유합니다. 창의 경우 `e.requestId`는 열 때 사용한 `id`입니다.

두 사이트는 모드가 채울 때까지 비어 있으며, 창과 밴드입니다. 탭을 선택하여 각각이 무엇인지, 그리고 어떻게 그리는지 확인합니다:

<Tabs>
  <Tab title="Pane">
    창은 넓은 전체 화면 터미널에서 대화 옆의 사이드바이거나, 그렇지 않으면 프롬프트 위의 프레임 영역입니다. 여러 창이 열려 있으면 각각 제목을 표시하는 탭을 가집니다.

    창은 모드가 `$.ui.open`을 `id`와 함께 호출할 때 나타나며, 예를 들어 `$.ui.open({ id: 'hello-tabs' })`입니다. [올바른 시간에 창 열기](#open-a-pane-at-the-right-time)는 다른 필드와 창이 더 넓은 터미널을 기다릴 때를 다룹니다.

    창에 그리려면 `{ component: 'Pane' }`으로 필터링하고 `e.requestId`가 `id`인지 확인합니다.
  </Tab>

  <Tab title="Band above the prompt">
    밴드는 프롬프트 입력 바로 위의 스트립입니다. 항상 있으며 모든 모드가 공유합니다.

    훅은 밴드에 무언가를 표시하는 트리를 반환하거나, 아무것도 표시하지 않으려면 `next(e)`를 반환합니다. 트리는 모드 [이후의 것들](/docs/ko/plugins/mods/events#the-order-mods-run-in)이 그곳에 그리는 것을 대체합니다. 그들의 것을 유지하려면 `await next(e)`의 결과를 트리의 [`Box`](#build-a-tree-from-elements)의 자식 중에 넣습니다.

    밴드에 그리려면 `{ component: 'AbovePrompt' }`으로 필터링합니다.
  </Tab>
</Tabs>

<h3 id="change-what-claude-code-already-draws">
  Claude Code가 이미 그리는 것 변경
</h3>

Claude Code는 대부분의 인터페이스를 자체적으로 그립니다: 메시지, 도구 호출 행, 스피너 등. 이러한 각 부분도 렌더 사이트이므로 모드는 이를 다시 스타일링하거나 대체할 수 있습니다. 하나를 변경하려면 이 표의 이름으로 `ui.render` 훅을 필터링합니다:

| Site | What it is |
| :- | :- |
| `UserMessage`, `AssistantMessage` | 대화의 메시지 |
| `ToolUse`, `ToolResult`, `ToolGroup` | 도구 호출의 행, 그 결과, 그리고 접힌 호출 실행 |
| `CommandOutput` | 명령이 인쇄한 행 |
| `AskUserQuestion` | Claude가 질문을 하기 위해 열 수 있는 대화 |
| `Spinner`, `ToolProgress`, `TurnDuration` | 턴의 상태 줄: Claude가 작업하는 동안 애니메이션되는 줄, 실행 중인 도구의 실시간 진행 줄, 턴을 닫는 줄 |
| `InfoNotice`, `SessionMode`, `PromptHint` | 로고 아래의 상태 줄, 바닥글의 모드 레이블, 프롬프트 아래의 힌트 줄 |

Claude Code가 이미 그리는 사이트에서 훅에는 세 가지 선택이 있습니다: 세부 사항 변경, 그리기 대체, 또는 그대로 두기. 탭을 선택하여 각각을 스피너에 적용한 것을 확인합니다. 예제는 [튜토리얼 모드](/docs/ko/plugins/mods/create#write-a-mod-yourself)처럼 다른 훅이 계산하는 `calls` 변수를 읽습니다.

<Tabs>
  <Tab title="Change a detail">
    Claude Code의 그리기를 유지하고 그 일부를 변경하려면 변경된 `props`로 이벤트의 복사본을 `next`에 전달합니다. 이 훅은 스피너의 단어 뒤의 텍스트를 변경합니다:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
      // Keep Claude Code's spinner, and change the text after its word
      return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
    })
    ```

    스피너는 애니메이션과 단어를 유지하고 텍스트가 단어를 따릅니다:

    ```text theme={null}
    Thinking · tool calls: 2…
    ```
  </Tab>

  <Tab title="Replace the drawing">
    사이트 위치에 자신의 것을 그리려면 트리를 반환하고 `next`를 호출하지 마십시오. 이 훅은 스피너가 있을 위치에 한 줄의 텍스트를 그립니다:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e) => {
      const { Text } = $.ui.resolve(e)
      // No call to next, so this line is drawn in the spinner's place
      return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
    })
    ```

    Claude가 작업하는 동안 줄이 표시되고 Claude Code의 스피너는 표시되지 않습니다:

    ```text theme={null}
    Claude has made 2 tool calls
    ```
  </Tab>

  <Tab title="Leave it alone">
    사이트를 Claude Code가 그리는 대로 두려면 `next(e)`를 반환합니다. 훅은 종종 일부 이벤트에 대해서는 그렇게 하고 다른 이벤트에 대해서는 그렇지 않습니다. 이 훅은 계산할 호출이 있을 때까지 스피너를 그대로 둡니다:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
      // Nothing to show yet, so pass the event on unchanged
      if (calls === 0) return next(e)
      return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
    })
    ```

    첫 번째 도구 호출 전에 스피너는 모드 없이 하는 방식으로 보입니다:

    ```text theme={null}
    Thinking…
    ```
  </Tab>
</Tabs>

권한 프롬프트는 렌더 사이트가 아니므로 모드는 표시되는 것을 변경할 수 없습니다. 질문 대화 `AskUserQuestion`은 하나이므로 모드는 그것을 변경할 수 있습니다.

터미널과 Desktop 앱은 모두 동일한 사이트를 발생시키지 않습니다. `Pane`, `AbovePrompt`, `Spinner`, 그리고 대화 사이트는 둘 다에서 작동합니다. 몇 가지 다른 상태 줄은 터미널에서만 발생합니다. [렌더 사이트 표](/docs/ko/plugins/mods/reference#render-sites)는 각각이 발생하는 위치를 나열합니다.

<h3 id="open-a-pane-at-the-right-time">
  올바른 시간에 창 열기
</h3>

창은 모드가 열 때만 나타납니다. 어떻게 그리고 언제 열 것인지는 키보드 포커스를 받는지, 얼마나 많은 공간을 요청하는지, 그리고 좁은 터미널에서 전혀 표시되는지 여부를 결정합니다.

창을 열려면 선택한 `id`로 [`$.ui.open`](/docs/ko/plugins/mods/reference#mods-api-methods)을 호출합니다. `id`는 창의 이름입니다: `ui.render` 훅이 확인하고, 창을 닫을 때 다시 전달합니다.

```javascript theme={null}
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
```

창을 닫으려면 열 때 사용한 `id`로 `$.ui.close`를 호출합니다:

```javascript theme={null}
await $.ui.close({ id: 'hello-tabs' })
```

`id` 외에도 `$.ui.open`은 다음 선택적 필드를 사용합니다:

| Field | What it does |
| :- | :- |
| `title` | 둘 이상의 창이 열려 있을 때 창의 탭 레이블 |
| `focus` | [키보드 포커스](#know-which-keys-your-mod-can-receive) 요청 |
| `closeOnEscape` | Esc가 창을 닫게 합니다. `true`를 전달하거나 필드를 생략합니다. Claude Code는 `false`를 거부하기 때문입니다. |
| `holdToasts` | 창이 닫힐 때까지 [`$.ui.toast`](/docs/ko/plugins/mods/api#show-something-without-starting-a-turn)의 작은 알림인 토스트를 유지합니다 |
| `rows` | 창이 프롬프트 위에 있을 때 요청할 높이입니다. 기본값은 공간의 1/3입니다. |
| `columns` | 창이 대화 옆에 있을 때 요청할 너비입니다 |

Claude가 작업하는 동안 명령이 창을 열 수 있도록 하려면 [명령을 등록](/docs/ko/plugins/mods/api#add-a-command)할 때 `immediate: true`를 추가합니다. 없으면 턴 중에 입력된 명령은 턴이 끝날 때까지 기다립니다.

<h4 id="when-a-pane-waits-for-a-wider-terminal">
  창이 더 넓은 터미널을 기다릴 때
</h4>

모드가 요청받지 않고 열 수 있는 창은 좁은 터미널에 나타나지 않으므로 작은 화면을 차지할 수 없습니다. 나타나는지 여부는 열 수 있는 것에 따라 다릅니다:

* **사용자가 한 것**, 예를 들어 실행한 명령이나 누른 버튼과 같이 사용자가 한 것으로 열린 경우, 창은 모든 너비에서 나타납니다
* **모드가 자체적으로 작동하여 열린 경우**, 예를 들어 타이머 또는 [`turn.start`](/docs/ko/plugins/mods/events#follow-a-turn) 훅에서, 창은 최소 144개 열의 터미널에서만 나타납니다. 사용자가 그 창을 직접 한 번 열었으면 110개 열이면 충분합니다.

창이 나타나면 `$.ui.open`은 `{ isPlaced: true }`로 해결됩니다. 창이 대기 중이면 `isPlaced`는 `false`이고 `reason`은 이유를 설명하는 문자열입니다. 대기 중인 창은 사용자가 열거나 터미널을 넓힐 때 나타납니다. 창을 열지 않고 무언가를 사용할 수 있다고 말하려면 `$.ui.toast('Your message')`를 호출합니다. 이는 몇 초 후에 사라지는 작은 알림을 표시합니다.

<h2 id="build-a-tree-from-elements">
  요소에서 트리 만들기
</h2>

`ui.render` 훅이 반환하는 것은 요소 트리입니다: 상자, 텍스트, 그리고 서로 중첩된 컨트롤로 만든 그릴 것의 설명. 그리기를 설명하고, Claude Code는 터미널이나 데스크톱 앱에서 렌더링합니다.

요소를 얻으려면 훅에서 `$.ui.resolve(e)`를 호출하세요. 예를 들어 `const { Box, Text, Button } = $.ui.resolve(e)`. 각 요소는 함수입니다. 속성을 전달하고, 그 안에 가는 요소와 문자열을 `children`에 넣습니다.

대부분의 그리기는 네 가지 요소를 사용합니다. 탭을 선택하여 각각과 터미널이 어떻게 그리는지 보세요:

<Tabs>
  <Tab title="텍스트">
    `Text`는 `bold`와 `color` 같은 선택적 스타일링으로 문자열을 그립니다:

    ```javascript theme={null}
    Text({ children: ['This is the first tab.'] })
    ```

    ```text theme={null}
    This is the first tab.
    ```
  </Tab>

  <Tab title="상자">
    `Box`는 그 안에 있는 것을 행이나 열로 배열합니다. 이것은 버튼과 텍스트 줄을 나란히 놓고, 두 열 떨어져 있습니다:

    ```javascript theme={null}
    Box({
      flexDirection: 'row',
      columnGap: 2,
      children: [
        Button({ key: 'more', label: 'Add one', onPress: addOne }),
        Text({ children: ['Count: 0'] }),
      ],
    })
    ```

    ```text theme={null}
    [ Add one ]  Count: 0
    ```
  </Tab>

  <Tab title="버튼">
    `Button`은 사용자가 누를 수 있는 컨트롤입니다. `onPress` 콜백을 실행합니다. `plain: true`로 괄호가 없고 핫키를 표시합니다:

    ```javascript theme={null}
    Button({ key: 'more', label: 'Add one', onPress: addOne })
    Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
    ```

    ```text theme={null}
    [ Add one ]
    1: One
    ```
  </Tab>

  <Tab title="입력">
    `Input`은 텍스트 필드입니다. 사용자가 Enter를 누르면 텍스트로 `onSubmit` 콜백을 실행합니다:

    ```javascript theme={null}
    Input({
      key: 'new-note',
      label: 'Note',
      placeholder: 'Type a note and press Enter',
      value: '',
      submitLabel: 'add',
      onSubmit: addNote,
    })
    ```

    ```text theme={null}
    Note: Type a note and press Enter ⏎ add
    ```
  </Tab>
</Tabs>

이 표는 모든 요소를 나열합니다:

| 요소 | 무엇을 그리는가 | 어디서 |
| :- | :- | :- |
| `Box` | 플렉스 컨테이너. `flexDirection`, `columnGap`, `padding`, `borderStyle`, `width` 같은 레이아웃 속성을 사용합니다. | 모든 곳 |
| `Text` | 스타일이 지정된 텍스트. `color`, `bold`, `dimColor`, `italic`, `wrap`을 사용합니다. `color`는 테마 키 또는 `'red'` 같은 색입니다. `wrap`은 `'wrap'`, `'truncate'`, `'truncate-start'`, `'truncate-middle'`, 또는 `'truncate-end'`입니다. | 모든 곳 |
| `Button` | `onPress`를 호출하는 컨트롤 | 모든 곳 |
| `Link`, `Code`, `Markdown` | `href`와 선택적 `label`이 있는 링크, 코드 블록, Claude의 답변 방식으로 포맷된 텍스트. `Markdown`은 `children`이 아닌 `text` 속성에서 내용을 가져오고, `onLinkPress`를 전달할 때 `key`가 필요합니다. | 모든 곳 |
| `Input`, `Select` | 텍스트 필드와 선택기 | 터미널, 데스크톱 |
| `Svg` | SVG 문서 | 데스크톱 |
| `Client` | 두 번째 파일로 그려진 영역, 애니메이션과 포인터 입력용. 그 파일은 모드 API를 받지 않습니다. 데이터를 게시하여 훅에만 도달하며, 이는 `ui.message` 이벤트로 도착합니다. | 터미널, 데스크톱 |
| `Raster`, `Image` | [색상 셀의 그리드](#draw-a-grid-of-colored-cells), 그리고 그림 | 터미널 |

모듈이 `.tsx` 또는 `.jsx` 파일이면 트리를 JSX로 쓸 수 있습니다. `$.ui.resolve(e)`에서 요소를 분해하세요. 훅 모듈에는 요소 전역이 없기 때문입니다.

트리가 앱이 없는 요소, 요소가 사용하지 않는 속성, 또는 아무것도 가지 않는 곳에 자식을 사용하면 Claude Code는 자신의 버전의 사이트를 그립니다.

`--plugin-dir`로 시작한 세션에서 대화 기록 줄은 그렇게 말합니다. 예를 들어 `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`. [디버그 로그](/docs/ko/plugins/mods/troubleshoot#read-the-debug-log)는 `ui.render (Pane): a hook returned a tree that does not validate`로 같은 이유로 기록합니다. 세션에 다른 것도 나타나지 않으므로 그리기가 표시되지 않으면 그 줄이나 로그를 확인하세요.

<h3 id="draw-a-grid-of-colored-cells">
  색상 셀의 그리드 그리기
</h3>

열 지도, 스파크라인, 또는 터미널의 게임 보드의 경우 각 셀에 대해 하나의 `Box`가 아닌 하나의 `Raster`를 그리세요. `Raster`는 `key`, `columns`과 `rows`의 크기, 그리고 모든 셀을 하나의 문자열로 압축하는 `cells`를 사용합니다. 각 셀은 세 개의 숫자입니다: 문자의 코드 포인트, 색, 배경색. 색은 빨강, 녹색, 파랑 각각 두 자리의 16진수 숫자입니다. 예를 들어 빨강의 경우 `0xc62828`, 터미널의 기본값의 경우 `0x01000000`.

데스크톱 앱에는 `Raster`가 없으므로 `e.surface`를 확인하고 거기에 텍스트를 그리세요. 이 창 본문은 3x2 열 지도를 그립니다:

```javascript theme={null}
// "터미널의 기본 색을 사용"을 의미하는 값
const DEFAULT_COLOR = 0x01000000

// [문자, 색] 쌍의 행을 Raster가 사용하는 하나의 문자열로 압축
// 한 셀은 세 개의 숫자: 문자의 코드 포인트, 색, 배경색
function cellsOf(rows) {
  const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
  return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
}

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  // id가 'heat'인 창에서만 그리기
  if (e.requestId !== 'heat') return next(e)
  const { Box, Text, Raster } = $.ui.resolve(e)
  // 3개의 셀 각각이 블록 문자와 색인 2행
  const rows = [
    [['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
    [['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
  ]
  if (e.surface !== 'terminal') {
    return Text({ children: ['The heat map needs the terminal.'] })
  }
  return Box({
    flexDirection: 'column',
    children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
  })
})
```

터미널에서 창은 그리드를 표시합니다:

<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-heat-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=b91bcce3bad74bc851149133d4acc5d5" alt="색상 블록의 작은 그리드를 보유하는 터미널의 창, 2행 3개. 맨 위 행은 녹색, 호박색, 빨강입니다. 아래 행은 녹색, 녹색, 호박색입니다." width="360" height="132" data-path="images/mods-heat-map.svg" />

`rows` 배열은 변경할 부분이고, `cellsOf`는 그것을 압축된 문자열로 변환합니다. 훅은 `id`가 `heat`인 창에서만 그리므로 [`hello-tabs` 예제](#build-a-pane-with-tabs)가 창을 열 때처럼 명령에서 `$.ui.open({ id: 'heat' })`로 하나를 열어야 합니다.

각 문자는 한 셀 너비여야 합니다. 이미 화면에 있는 `Raster`를 애니메이션하려면 창의 `id`를 `requestId`로, `Raster`의 `key`를 `key`로, 같은 크기, 그리고 새로운 셀로 `$.ui.blit`을 호출하세요. 이 예제의 경우 `$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })`입니다. 그것은 `ui.render` 훅을 다시 실행하지 않고 그 하나의 요소를 다시 칠합니다.

<h2 id="respond-to-presses-and-typing">
  누름과 입력에 응답
</h2>

사용자가 버튼을 누르거나, 필드에 입력하거나, 모드가 그린 목록에서 선택하면 Claude Code는 해당 컨트롤에 준 함수를 호출하고, 모듈에서 실행됩니다. 각 컨트롤은 자신의 콜백을 사용합니다:

* **`Button`**: `onPress(e)`를 사용합니다. 여기서 `e.surface`는 누름이 온 앱입니다
* **`Input`**: `onSubmit(value)`와 `onInput(value)`를 사용합니다
* **`Select`**: `onSelect(value)`를 사용합니다. 선택지는 `options`에 있으며, 고유한 값을 가진 최소 하나의 선택지 목록입니다. 예를 들어 `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`

테스트는 `key`로 컨트롤을 누르거나 입력하므로 각각에 하나를 주세요. 컨트롤의 각 사용은 또한 [`ui.press`, `ui.input`, 또는 `ui.select`](/docs/ko/plugins/mods/reference#interface)를 `e.element`의 `key`로 발생시키고, 다른 모드는 이러한 이벤트를 훅할 수 있습니다. 그 훅은 콜백 전에 실행되므로 사용자가 `Input`에 입력하는 것을 보고 변경하거나 콜백 대신 답할 수 있습니다. 모드 API에는 다른 모드의 버튼을 누르는 메서드가 없습니다.

<h3 id="know-which-keys-your-mod-can-receive">
  키보드 포커스와 핫키
</h3>

모드는 절대 키보드를 직접 읽지 않습니다. 사용자가 키를 누르고, Claude Code는 어느 컨트롤이 그것인지 결정하고, 그 컨트롤의 콜백이 실행됩니다. [밴드의 숫자 핫키](/docs/ko/plugins/mods/reference#elements)를 제외하고, 이는 창이나 밴드가 키보드 포커스를 가질 때만 발생합니다. 나머지 시간에는 키가 프롬프트로 갑니다.

<h4 id="how-a-pane-gets-keyboard-focus">
  창이 키보드 포커스를 얻는 방법
</h4>

창은 세 가지 방법 중 하나로 키보드 포커스를 얻습니다:

* 모드가 명령이나 누름에서 `focus: true`로 열기
* 사용자가 Ctrl+X를 누른 다음 Tab
* 사용자가 클릭

Claude Code는 프롬프트가 비어 있고 다른 것이 키보드 포커스를 가지지 않을 때만 `focus: true`를 부여합니다. 사용자가 입력하는 동안 열리는 창은 그들의 키 입력을 가져가지 않습니다.

<h4 id="what-each-key-does">
  각 키가 하는 것
</h4>

이 표는 창이나 밴드가 키보드 포커스를 가질 때 키가 하는 것을 나열합니다:

| 키 | 무엇을 하는가 |
| :- | :- |
| Tab | 다음 컨트롤로 이동 |
| 위 및 아래 | 그리기가 맞을 때 컨트롤 간 이동. 창이나 밴드가 표시할 수 있는 것보다 더 많은 행을 가지면 스크롤합니다. |
| Enter | 포커스된 `Button`을 누르거나, 포커스된 `Input`을 제출하거나, `Select`에서 선택 |
| 버튼의 핫키 | 그 버튼을 누릅니다. `Input`이 포커스를 가질 때 모든 인쇄 가능한 키는 필드로 갑니다. |
| Esc | 키보드 포커스를 프롬프트로 반환합니다. `closeOnEscape: true`로 창도 닫습니다. |

모드는 Tab이나 화살표 키를 다른 것에 바인드할 수 없으므로 게임은 `w`, `a`, `s`, `d`로 조종합니다.

<h4 id="set-a-hotkey-and-the-first-focus">
  핫키와 첫 포커스 설정
</h4>

컨트롤의 두 속성이 키보드가 어떻게 도달하는지 결정합니다:

* **`hotkey`**: 사용자가 `Button`을 한 키로 누르도록 하려면 `hotkey: 'a'`처럼 한 자리 또는 한 소문자의 `hotkey`를 주세요
* **`autoFocus`**: 창이 열릴 때 어느 컨트롤이 포커스를 가지는지 선택하려면 `autoFocus: true`를 추가하세요. 다른 것에서는 속성을 생략하세요. Claude Code는 `autoFocus: false`를 거부합니다.

핫키가 표시되는 방식은 버튼과 앱에 따라 다릅니다:

| 버튼 | 터미널에서 | 데스크톱 앱에서 |
| :- | :- | :- |
| 괄호 포함, 기본값 | `[ Add one ]`, 핫키 표시 없음 | 옆에 작은 키가 있는 레이블 |
| `plain: true` 포함 | `1: One` | 옆에 작은 키가 있는 레이블 |

터미널에서 괄호가 있는 버튼의 레이블에 키의 이름을 지정하거나 `plain: true`를 사용하여 사용자가 누를 것을 볼 수 있도록 하세요. [요소 참조](/docs/ko/plugins/mods/reference#elements)는 다른 `Button` 규칙을 가집니다: `action`, 밴드의 숫자 핫키, 그리고 한 핫키의 두 버튼.

<h3 id="take-typed-input-and-draw-a-row-for-each-item">
  입력된 텍스트를 가져오고 각 항목에 대해 행을 그리기
</h3>

많은 창은 텍스트 필드와 그 아래 목록입니다. 이 섹션의 예제는 노트 창입니다: 노트를 입력하고 Enter를 눌러 추가하고, 각 노트에는 삭제하는 `x` 버튼이 있습니다. 두 개의 노트가 추가되면 터미널은 창을 이렇게 그립니다:

```text theme={null}
╭──────────────────────────────────────────────────────────╮
│ Note: Type a note and press Enter ⏎ add                ✕ │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯
```

예제는 두 가지 기술을 사용합니다:

* **입력된 텍스트 가져오기**: `Input`은 사용자가 Enter를 누르면 필드의 텍스트로 `onSubmit(value)`를 호출하고, 모든 변경에 `onInput(value)`를 호출합니다
* **목록 그리기**: 데이터를 각각 하나의 행으로 매핑하고, 모든 행의 버튼에 자신의 `key`를 주세요

이 훅은 창의 내용을 그립니다:

```javascript theme={null}
// 창이 그리는 목록
let notes = []

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  // id가 'notes'인 창에서만 그리기
  if (e.requestId !== 'notes') return next(e)
  const { Box, Text, Button, Input } = $.ui.resolve(e)
  const redraw = () => $.ui.invalidate('ui.render')

  return Box({
    flexDirection: 'column',
    children: [
      Input({
        key: 'new-note',
        label: 'Note',
        placeholder: 'Type a note and press Enter',
        // 매번 필드를 비워서 그리기, 제출 후 지우기
        value: '',
        submitLabel: 'add',
        autoFocus: true,
        // 필드에서 Enter를 누를 때 실행
        onSubmit: async (value) => {
          // 빈 줄 무시
          if (!value.trim()) return
          notes = [...notes, value.trim()]
          redraw()
          await $.store.set('notes', notes)
        },
      }),
      // 각 노트에 대해 한 행: 삭제 버튼, 그 다음 노트의 텍스트
      ...notes.map((note, i) =>
        Box({
          flexDirection: 'row',
          columnGap: 1,
          children: [
            Button({
              // 자신의 키, 각 행의 버튼을 구별할 수 있도록
              key: 'delete-' + i,
              label: 'x',
              plain: true,
              onPress: async () => {
                notes = notes.filter((_, j) => j !== i)
                redraw()
                await $.store.set('notes', notes)
              },
            }),
            Text({ children: [note] }),
          ],
        }),
      ),
    ],
  })
})
```

창을 시도하려면:

* **노트 추가**: 줄을 입력하고 Enter를 누르세요. 줄이 새 행으로 나타나고 필드가 비워집니다.
* **노트 삭제**: Tab을 누르다가 노트의 `x` 버튼이 포커스를 가질 때까지, 그 다음 Enter를 누르세요. `x`는 버튼의 레이블이고 핫키가 아니므로 문자를 입력해도 누르지 않습니다.

각 변경은 `hello-tabs`와 같은 렌더 사이클을 따릅니다: 콜백이 `notes`를 변경하고, `redraw`를 호출하고, 목록을 `$.store`에 저장합니다.

필드는 `value` 속성 때문에 각 제출 후 비워집니다. `value`는 필드가 그려질 때 보유하는 텍스트이고, 사용자의 입력은 훅이 필드를 다시 그릴 때까지 그것을 대체합니다. 예제는 항상 필드를 `''`로 그립니다.

예제는 노트를 저장하고 로드하지 않습니다. 다음 세션에서 그들을 다시 가져오려면 `hello-tabs`가 `count`를 읽는 방식처럼 `session.start` 훅에서 읽으세요.

세 가지 속성이 필드의 줄을 구성합니다. `Note: Type a note and press Enter ⏎ add`:

| 속성 | 예제에서 | 무엇인가 |
| :- | :- | :- |
| `label` | `Note` | 필드 앞의 텍스트. 터미널은 그 뒤에 `: `를 그립니다. |
| `placeholder` | `Type a note and press Enter` | 필드가 비어 있는 동안 표시되는 흐린 텍스트 |
| `submitLabel` | `add` | `⏎` 뒤의 단어로 Enter가 하는 것을 말합니다 |

`Input`을 제출해도 [`$.prompt.submit`](/docs/ko/plugins/mods/api#start-a-turn-from-a-background-job)을 호출하지 않으면 턴을 시작하지 않습니다.

<h2 id="redraw-when-something-changes">
  사이트 다시 그리기
</h2>

그리기는 스냅샷입니다: 훅이 마지막으로 실행되었을 때 `ui.render` 훅이 반환한 것을 보여줍니다. 새로운 것을 표시하려면 훅이 다시 실행되어야 합니다. Claude Code는 일부 변경에 대해 다시 실행하고, 모드는 나머지를 요청합니다.

<h3 id="when-claude-code-redraws-without-being-asked">
  Claude Code가 요청받지 않고 다시 그릴 때
</h3>

Claude Code는 사이트의 속성이 변경되거나 터미널의 너비가 변경될 때 `ui.render` 훅을 다시 실행합니다. 타이머에서 훅을 실행하지 않고, 모듈의 변수가 변경되는 것을 알 수 없습니다.

<h3 id="redraw-when-your-data-changes">
  데이터가 변경될 때 다시 그리기
</h3>

데이터가 변경된 후 사이트를 다시 그리려면 `$.ui.invalidate('ui.render')`를 호출하세요. 이 창은 누름을 세습니다. 버튼의 콜백은 `count`를 변경한 다음 다시 그리기를 요청합니다:

```javascript theme={null}
let count = 0

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  if (e.requestId !== 'counter') return next(e)
  const { Box, Text, Button } = $.ui.resolve(e)
  return Box({
    flexDirection: 'row',
    columnGap: 2,
    children: [
      Button({
        key: 'more',
        label: 'Add one',
        onPress: () => {
          count += 1
          // 데이터가 변경되었으므로 Claude Code에 창을 다시 그리도록 요청
          $.ui.invalidate('ui.render')
        },
      }),
      Text({ children: ['Count: ' + count] }),
    ],
  })
})
```

각 누름은 창의 숫자를 올립니다. [`hello-tabs` 예제](#build-a-pane-with-tabs)는 같은 호출을 `redraw` 함수로 래핑합니다.

[`$.state`](#keep-a-value-in-\$-state)에 보관하는 값은 호출이 필요하지 않습니다. 값을 쓰면 그것을 읽는 사이트가 다시 그려지기 때문입니다.

<h3 id="redraw-on-a-timer">
  타이머에서 다시 그리기
</h3>

시계, 카운트다운, 또는 세션 외부의 값을 최신 상태로 유지하려면 일정에 따라 다시 그리세요. 모듈의 `session.start` 훅에서 타이머를 시작하세요. 모듈이 이미 하나를 가지고 있으면, `hello-tabs`처럼, [`$.clock.every`](/docs/ko/plugins/mods/api#run-work-in-the-background) 줄을 추가하세요:

```javascript theme={null}
on('session.start', async ($, e, next) => {
  // 1000밀리초마다 Claude Code에 사이트를 다시 그리도록 요청
  $.clock.every(1000, () => $.ui.invalidate('ui.render'))
  return next(e)
})
```

Claude Code는 이제 `ui.render` 훅을 초당 한 번 실행합니다. 모듈이 다시 로드되면 타이머가 중지되고, 새 복사본이 자신의 것을 시작합니다.

<h3 id="how-often-a-site-can-redraw">
  사이트가 얼마나 자주 다시 그릴 수 있는가
</h3>

Claude Code는 사이트가 얼마나 자주 다시 그려지는지 제한하므로 모드는 데이터가 변경될 때마다 `$.ui.invalidate`를 호출할 수 있습니다. 보이는 창과 밴드는 다른 사이트보다 높은 제한을 가지고, [제한 표](/docs/ko/plugins/mods/reference#limits)는 숫자를 가집니다.

제한보다 빠르게 오는 호출은 하나의 다시 그리기로 결합됩니다. 그 다시 그리기는 훅을 한 번 실행하고, 훅은 그 순간의 데이터를 읽으므로 최신 값이 표시되고 그 사이의 값은 표시되지 않습니다. 애니메이션은 제한보다 빠르게 실행될 수 없습니다.

<h2 id="keep-state">
  상태 유지
</h2>

모드는 값을 유지하는 세 곳이 있고, 값이 얼마나 오래 지속되는지에 따라 다릅니다: 모듈이 다시 로드될 때까지, 세션이 끝날 때까지, 또는 한 세션에서 다음 세션까지. 값이 얼마나 오래 지속되어야 하는지에 따라 선택하세요:

| 여기에 유지 | 지속되는 기간 | 사용 대상 |
| :- | :- | :- |
| 모듈 수준 변수 | 모듈이 다시 로드될 때까지. 개발 중에 파일을 저장할 때마다 발생 | `hello-tabs`의 `tab`처럼 잃을 수 있는 값 |
| `$.state` | 세션이 끝나거나 사용자가 `/clear`, `/resume`, 또는 `/branch`를 실행할 때까지 | 그리기가 의존하는 값으로 다시 로드를 생존해야 함 |
| `$.store` | 모드가 삭제하거나, 세션이 [`cleanupPeriodDays`](/docs/ko/settings-reference#cleanupperioddays) 동안 저장소를 읽거나 쓰지 않을 때까지. 저장소는 `~/.claude/plugins/store/` 아래 플러그인 자신의 JSON 파일로 저장되는 키-값 저장소입니다. | 설정, 기록, 사용자가 다음에 찾을 것으로 예상하는 모든 것 |

`$.store.get(key)`는 값 또는 `undefined`로 해결되고, `$.store.set(key, value)`는 모든 JSON 값을 사용합니다.

<h3 id="keep-a-value-in-state">
  `$.state`에 값 유지
</h3>

`$.state`는 세션 길이의 값을 유지하고 당신을 위해 다시 그립니다. 이는 반응형 상태입니다: 값을 읽는 `ui.render` 훅이 그것을 구독하므로 Claude Code는 값을 쓸 때마다 그 사이트를 다시 그리고, `$.ui.invalidate`를 호출할 필요가 없습니다. `$.state`의 값은 또한 변수가 하지 않는 모듈의 다시 로드를 생존합니다.

설정하려면 값을 선언하고, 매니페스트를 선언으로 가리키고, 각 값을 정의하고 사용하세요. 예제는 `hello-tabs`에서 `count`를 `$.state`로 이동합니다.

<h4 id="declare-the-values">
  값 선언
</h4>

타입 파일에서 값을 선언하세요. 외부 키는 플러그인의 이름이고, 그 아래의 각 항목은 값과 그 타입입니다. 이를 `hello-tabs/types/index.d.ts`로 저장하세요:

```typescript hello-tabs/types/index.d.ts theme={null}
declare module 'claude-code' {
  interface PluginState {
    'hello-tabs': {
      tab: 'one' | 'two'
      count: number
    }
  }
}
```

<h4 id="point-the-manifest-at-the-declaration">
  매니페스트를 선언으로 가리키기
</h4>

`claude plugin validate`가 코드를 해당 파일에 대해 확인하도록 하려면 선언의 경로로 매니페스트에 `types` 필드를 추가하세요:

```json hello-tabs/.claude-plugin/plugin.json theme={null}
{
  "name": "hello-tabs",
  "version": "0.1.0",
  "description": "Opens a pane with two tabs and a counter",
  "author": { "name": "Your Name" },
  "types": "./types/index.d.ts"
}
```

<h4 id="define-read-and-write-a-value">
  값 정의, 읽기, 쓰기
</h4>

모듈에서 각 값을 기본값으로 정의하고, 그리는 동안 읽고, 콜백에서 쓰세요. `atom`은 값과 기본값의 이름을 지정하고, `read`는 반환하고, `update`는 씁니다. 세 가지 도우미는 `$.state.get`과 `$.state.set`을 호출합니다:

```javascript theme={null}
import { atom, read, update } from 'claude-code'

// 모듈의 맨 위: 값의 이름을 지정하고 기본값을 주기
const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)

// ui.render 훅에서: 값을 읽어 그리기
const n = await read($, count)

// 버튼에서: 이전 값에서 새 값을 쓰기
onPress: () => update($, count, (value) => value + 1)
```

`ui.render` 훅이 `count`를 읽었기 때문에 Claude Code는 버튼이 그것을 쓸 때마다 훅을 다시 실행합니다.

코드에 세 가지 규칙이 적용됩니다:

* **`plugin`과 `key`를 리터럴 문자열로 쓰기**: `claude plugin validate`는 소스에서 읽습니다
* **타입 파일에서 모든 값을 선언하기**: 그렇지 않으면 검증이 `hello-tabs.count is not declared`로 실패합니다
* **콜백이나 다른 이벤트의 훅에서 쓰기**: `ui.render` 훅은 상태를 읽을 수 있고 쓸 수 없으므로 `onPress`, `onSubmit`, 또는 다른 이벤트의 훅에서 쓰세요

<h4 id="change-hello-tabs-to-use-state">
  `hello-tabs`를 `$.state` 사용으로 변경
</h4>

`hello-tabs`에서 `count`를 `$.state`로 이동하려면 그것을 사용하는 모든 줄을 변경하세요:

* **모듈의 맨 위**: `import` 줄을 추가하고, `let count = 0`을 `atom` 줄로 대체
* **`ui.render` 훅에서**: `tabButton` 전에 `read` 줄을 추가하고, `Text`에서 `'Count: ' + n`을 그리기
* **Add one 버튼에서**: `onPress`를 [한 세션 이상에서 저장](#save-from-more-than-one-session)의 것으로 대체합니다. 이는 카운트를 저장하고 쓰기도 합니다
* **`session.start` 훅에서**: `saved`를 읽는 두 줄을 [/clear 후 저장된 값 다시 로드](#load-a-saved-value-again-after-clear)의 `loadCount` 호출로 대체

`tab`이 여전히 변수이므로 `redraw`를 유지하세요.

<h3 id="load-a-saved-value-again-after-clear">
  `/clear` 후 저장된 값 다시 로드
</h3>

모드가 `$.store`에서 저장된 값을 `session.start`에서 `$.state`로 복사하면, `/clear`, `/resume`, 또는 `/branch` 후에 다시 복사해야 합니다. 이 명령은 모든 `$.state` 값을 기본값으로 되돌리고, `session.start`는 다시 발생하지 않습니다. [`classic.SessionStart`](/docs/ko/plugins/mods/events#hook-the-settings-hook-events)는 각각 후에 발생하고, `e.source`는 `clear`, `resume`, 또는 `fork`로 설정되므로 값을 다시 복사하세요. 그렇지 않으면 그리기는 기본값을 표시하고, `$.state` 값을 저장하는 콜백은 저장한 것 위에 기본값을 씁니다.

이 코드는 두 훅에서 `count`를 로드합니다. `$.state` 버전의 `hello-tabs`를 기반으로 하며, `count`는 원자이고 `update`는 가져옵니다. `loadCount`를 `register` 위에 놓고, 이미 가지고 있는 `session.start` 훅에 `loadCount` 호출을 추가하세요. `classic.SessionStart`는 또한 시작 시 그리고 압축 후에 발생하며, 이는 `$.state`를 재설정하지 않으므로 `source`의 필터는 훅을 세 가지 재설정으로 유지합니다:

```javascript theme={null}
// $.store에서 저장된 카운트를 $.state로 복사하거나, 아무것도 저장되지 않으면 0
async function loadCount($) {
  const saved = Number((await $.store.get('count')) ?? 0)
  await update($, count, () => saved)
}

// 첫 프롬프트 전에 실행되고, 다시 로드 후에도 실행됨
on('session.start', async ($, e, next) => {
  await loadCount($)
  return next(e)
})

// /clear, /resume, /branch 후에 다시 실행되며, fork를 보고합니다
on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
  await loadCount($)
  return next(e)
})
```

두 훅이 제자리에 있으면 창은 `/clear` 후 저장된 카운트를 표시하고 `0`이 아니며, **Add one**의 다음 누름은 저장된 카운트에 추가합니다.

`loadCount`는 `$.state`의 값 위에 저장된 값을 쓰고, `session.start`는 모듈이 다시 로드될 때마다 다시 발생합니다. 저장소가 뒤처지지 않도록 하려면 모든 변경에 저장하세요. **Add one** 버튼이 하는 것처럼.

다시 로드를 세션 없이 확인하려면 [`/clear` 후 그리기를 테스트](/docs/ko/plugins/mods/test#test-a-drawing-after-clear)하세요.

<h3 id="save-from-more-than-one-session">
  한 세션 이상에서 저장
</h3>

머신의 모든 세션이 모드를 실행하면 하나의 `$.store`를 공유합니다. `get` 다음에 `set`은 원자적이지 않습니다. 두 세션이 각각 값을 읽고, 변경하고, 다시 쓸 때 경쟁하고, 두 번째 쓰기가 첫 번째를 대체합니다.

두 가지 선택이 그것을 덜 가능하게 합니다:

* **각 항목에 자신의 키를 주기**: `set`은 자신의 키만 변경하므로 다른 키를 쓰는 세션은 서로를 덮어쓰지 않습니다
* **쓰기 바로 전에 다시 읽기**: 여러 세션이 변경하는 값의 경우, 콜백에서 키를 `get`하고 `session.start`에서 로드한 복사본이 아닌 그것에서 새 값을 빌드하세요. 다른 세션의 쓰기는 여전히 `get`과 `set` 사이에 착지하면 손실됩니다.

이 버튼은 저장소가 지금 보유하는 것에 1을 더한 다음 그리기를 업데이트합니다:

```javascript theme={null}
onPress: async () => {
  // 저장소가 지금 보유하는 것을 읽기, 다른 세션이 변경했을 수 있음
  const saved = Number((await $.store.get('count')) ?? 0)
  // 새 카운트를 저장한 다음 표시
  await $.store.set('count', saved + 1)
  await update($, count, () => saved + 1)
}
```

두 번째 세션이 이 세션이 시작된 이후로 자신의 버튼을 세 번 눌렀다면, 이 누름은 그 세 개를 포함하는 카운트를 표시하고 저장합니다.

<h2 id="next-steps">
  다음 단계
</h2>

* [이벤트에 반응](/docs/ko/plugins/mods/events): 도구 호출과 턴에서 그리기 공급
* [모드 API 사용](/docs/ko/plugins/mods/api): 타이머와 모델 호출에서 그리기 공급
* [그리기 테스트](/docs/ko/plugins/mods/test#test-a-drawing): 테스트에서 버튼을 누르기, 한 개 이상의 표면에서
* [렌더 사이트](/docs/ko/plugins/mods/reference#render-sites)와 [요소](/docs/ko/plugins/mods/reference#elements): 각 사이트의 속성과 각 요소의 속성
