Lesson 41

Theme and Visual Styling System

How Claude Code turns a ThemeName string into colored terminal output — from the six-palette type system to chalk level clamping to the /color command.

§1 The Big Picture

Terminal styling in Claude Code is not a thin wrapper around chalk. It is a deliberately layered system that must survive at least three hostile environments: VS Code's embedded terminal (which lies about its color support), tmux (which silently drops truecolor backgrounds), and Apple Terminal (which can't handle 24-bit SGR sequences at all). Each layer has a single job, and they compose cleanly.

Layer 1 — utils/theme.ts

Semantic Color Palette

6 named themes, each mapping ~70 semantic tokens to raw color values (RGB, hex, or ANSI). This is the source of truth for every color decision.

Layer 2 — ink/colorize.ts

Chalk Normalization

Detects terminal environment at module load time, boosts or clamps chalk's color level, then routes color strings to the right chalk method.

Layer 3 — ink/styles.ts

Layout + Style Types

Defines the Styles and TextStyles TypeScript types that Ink Box/Text components accept — the CSS-like API for terminal layout.

Layer 4 — design-system/color.ts

Theme-Aware Colorizer

Curried helper that accepts either a theme key (like "claude") or a raw color, resolves theme keys at call time, then delegates to colorize().

Commands — /theme, /color

User Controls

/theme opens an interactive picker. /color sets the prompt-bar color for sub-agent sessions — forbidden for swarm teammates.

AgentColorManager

Sub-agent Identity

Maps agent type strings to one of 8 theme color slots (_FOR_SUBAGENTS_ONLY) for visual differentiation in multi-agent sessions.

§2 The Theme Type System

The core data structure lives in utils/theme.ts. The Theme type is a flat record of ~70 named string slots. Every slot holds a raw color value — no nesting, no tokens-inside-tokens. This flatness is intentional: any component anywhere can look up a color in O(1) with zero risk of infinite resolution loops.

export type Theme = {
  // Brand identity
  claude: string          // rgb(215,119,87) — "Claude orange"
  claudeShimmer: string  // Lighter version for shimmer animation

  // UI surface roles
  promptBorder: string
  userMessageBackground: string
  selectionBg: string    // Alt-screen text selection highlight

  // Semantic roles
  success: string
  error: string
  warning: string

  // Diff colors (4 variants per operation)
  diffAdded: string
  diffAddedDimmed: string
  diffAddedWord: string

  // Agent colors — named to discourage general use
  red_FOR_SUBAGENTS_ONLY: string
  blue_FOR_SUBAGENTS_ONLY: string
  // ... 6 more

  // Rainbow colors for ultrathink keyword highlighting
  rainbow_red: string
  rainbow_red_shimmer: string
  // ... 12 more rainbow slots
}

The naming conventions tell a story. _FOR_SUBAGENTS_ONLY suffixes act as lint-time guardrails — you can visually grep for misuse. The Shimmer suffix signals that a color exists purely as the lighter step in a pulse animation, never for static text. The _FOR_SYSTEM_SPINNER suffix isolates the blue used by Claude's own spinner from user-visible permission prompts.

The Six Themes

There are exactly six concrete theme objects, mapped by the ThemeName union:

export const THEME_NAMES = [
  'dark',
  'light',
  'light-daltonized',
  'dark-daltonized',
  'light-ansi',
  'dark-ansi',
] as const

export const THEME_SETTINGS = ['auto', ...THEME_NAMES] as const
// ThemeSetting is stored in config; ThemeName is resolved at runtime

The auto setting is only valid in config storage — it is resolved to either dark or light by following the system's dark/light mode at runtime, then replaced by a concrete ThemeName before any component touches a color token.

Design note

The ANSI themes (light-ansi, dark-ansi) use only the 16 standard ANSI color names like ansi:redBright. This is important for terminals that don't support 256-color or truecolor — it means every color respects the user's own terminal palette customization. The RGB themes use explicit rgb(r,g,b) strings precisely to avoid being affected by custom terminal palettes.

§3 Dark vs. Light vs. Daltonized — What Actually Changes

The three families (dark, light, daltonized) differ in more than just brightness. The daltonized variants systematically replace green-red distinctions with blue-red ones, because deuteranopia (the most common form of color blindness) affects the green channel. Here's how a few key tokens change across the three dark variants:

Token dark dark-daltonized dark-ansi
claude rgb(215,119,87) rgb(255,153,51) ansi:redBright
success rgb(78,186,101) rgb(51,153,255) — blue! ansi:greenBright
diffAdded rgb(34,92,43) rgb(0,68,102) — dark blue! ansi:green
error rgb(255,107,128) rgb(255,102,102) ansi:redBright
selectionBg rgb(38,79,120) rgb(38,79,120) ansi:blue
autoAccept rgb(175,135,255) rgb(175,135,255) ansi:magentaBright

Notice that success in dark-daltonized is bright blue, not green. Someone with deuteranopia cannot rely on green to mean "good" — so the daltonized theme substitutes blue, which is on a completely different channel. The diffAdded token shifts from dark green to dark blue for the same reason.

Careful

The daltonized themes share the exact same selectionBg and autoAccept values as the regular dark theme — only the tokens that depend on green/red discrimination are swapped. This means you cannot just diff the two theme objects to understand which colors are "accessibility-critical": you have to reason about which tokens are used for semantic distinction vs. pure decoration.

§4 colorize.ts — The Terminal Environment Problem

This is where the engineering gets interesting. The file opens with two long block comments explaining two separate terminal environment bugs, and then fixes both at module load time — before any color is ever rendered.

Problem 1: VS Code Lies About Its Color Support

function boostChalkLevelForXtermJs(): boolean {
  // xterm.js has supported truecolor since 2017, but code-server/Coder
  // containers often don't set COLORTERM=truecolor. chalk's supports-color
  // doesn't recognize TERM_PROGRAM=vscode (it only knows iTerm.app/
  // Apple_Terminal), so it falls through to the -256color regex → level 2.
  // At level 2, chalk.rgb() downgrades to the nearest 6×6×6 cube color:
  // rgb(215,119,87) (Claude orange) → idx 174 rgb(215,135,135) — washed-out salmon.
  if (process.env.TERM_PROGRAM === 'vscode' && chalk.level === 2) {
    chalk.level = 3
    return true
  }
  return false
}

export const CHALK_BOOSTED_FOR_XTERMJS = boostChalkLevelForXtermJs()

The comment is worth reading closely. Claude's brand orange rgb(215,119,87) becomes a washed-out salmon rgb(215,135,135) in 256-color mode because the cube quantization rounds in the wrong direction. Rather than accept brand-color corruption in VS Code, the code manually bumps chalk to level 3 (truecolor) when it detects TERM_PROGRAM=vscode.

Problem 2: tmux Drops Truecolor Backgrounds

function clampChalkLevelForTmux(): boolean {
  // tmux parses truecolor SGR (\e[48;2;r;g;bm) into its cell buffer correctly,
  // but its client-side emitter only re-emits truecolor to the outer terminal
  // if the outer terminal advertises Tc/RGB capability. Default tmux config
  // doesn't set this. Without it, backgrounds are simply dropped — bg=default
  // → black on dark profiles.
  // Clamping to level 2 makes chalk emit 256-color (\e[48;5;Nm),
  // which tmux passes through cleanly. grey93 (255) is visually identical.
  if (process.env.CLAUDE_CODE_TMUX_TRUECOLOR) return false
  if (process.env.TMUX && chalk.level > 2) {
    chalk.level = 2
    return true
  }
  return false
}

export const CHALK_CLAMPED_FOR_TMUX = clampChalkLevelForTmux()

The ordering of these two calls matters: boostChalkLevelForXtermJs runs first. If someone is running Claude Code inside VS Code's terminal inside tmux (common in remote dev setups), the boost happens first and the clamp re-clamps it back to 2. The clamp wins over the boost for tmux, because tmux's passthrough limitation is a hard constraint that can't be worked around without reconfiguring tmux itself. The escape hatch is CLAUDE_CODE_TMUX_TRUECOLOR=1, which skips the clamp for users who have correctly configured terminal-overrides ,*:Tc in their tmux config.

Implementation detail

Both exports (CHALK_BOOSTED_FOR_XTERMJS and CHALK_CLAMPED_FOR_TMUX) are marked as exported for debugging. The comment says "tree-shaken if unused" — they exist so an engineer can import { CHALK_CLAMPED_FOR_TMUX } from './colorize' in a diagnostic and know whether the clamp fired, without adding any runtime cost in production builds where nothing imports them.

The colorize() Dispatch Table

With the chalk level set correctly, the actual color dispatch is a straightforward parser:

export const colorize = (
  str: string,
  color: string | undefined,
  type: ColorType,  // 'foreground' | 'background'
): string => {
  if (color.startsWith('ansi:')) {
    // Routes to chalk.red / chalk.bgRed etc.
    return type === 'foreground' ? chalk.red(str) : chalk.bgRed(str)
  }
  if (color.startsWith('#')) {
    return type === 'foreground'
      ? chalk.hex(color)(str)
      : chalk.bgHex(color)(str)
  }
  if (color.startsWith('ansi256')) {
    // Parses ansi256(N) → chalk.ansi256(N)
  }
  if (color.startsWith('rgb')) {
    // Parses rgb(r,g,b) → chalk.rgb(r,g,b)
  }
}

The string-prefix dispatch means the color format is self-describing. Any component that has resolved a theme token gets back a string that tells colorize exactly what kind of color it is — no separate type tag needed.

§5 styles.ts — Terminal Layout as TypeScript Types

The Styles type in ink/styles.ts is Claude Code's equivalent of a CSS properties object, but for terminal rendering. It covers layout (flexbox via Yoga), dimensions, borders, overflow, text wrapping, and color — all as readonly TypeScript properties.

export type TextStyles = {
  readonly color?: Color            // Raw color value, not a theme key
  readonly backgroundColor?: Color
  readonly dim?: boolean
  readonly bold?: boolean
  readonly italic?: boolean
  readonly underline?: boolean
  readonly strikethrough?: boolean
  readonly inverse?: boolean
}

// Color is a discriminated union of all supported formats:
export type Color = RGBColor | HexColor | Ansi256Color | AnsiColor
// where RGBColor = `rgb(${number},${number},${number})`
// and   AnsiColor = 'ansi:black' | 'ansi:red' | ... (16 ANSI names)

The key architectural decision here: TextStyles.color is always a raw Color value, never a theme key. The comment in the source is explicit: "Colors are raw values — theme resolution happens at the component layer." This means styles.ts and colorize.ts are completely unaware of themes. They are pure mechanics. Only the component layer (via design-system/color.ts) bridges from theme tokens to raw colors.

The Styles → Yoga Mapping

The default export of styles.ts is a function that applies a Styles object onto a LayoutNode (Yoga layout engine). This is what Ink calls when you write <Box flexDirection="row" padding={2}>:

const styles = (
  node: LayoutNode,
  style: Styles = {},
  resolvedStyle?: Styles,  // Full current style, for diff application
): void => {
  applyPositionStyles(node, style)
  applyOverflowStyles(node, style)
  applyMarginStyles(node, style)
  applyPaddingStyles(node, style)
  applyFlexStyles(node, style)
  applyDimensionStyles(node, style)
  applyDisplayStyles(node, style)
  applyBorderStyles(node, style, resolvedStyle)
  applyGapStyles(node, style)
}

The resolvedStyle parameter exists specifically for applyBorderStyles. When a style update is applied as a diff (only changed properties), borderStyle might be in the diff but borderTop might not be — because it didn't change. The resolved style carries the previous full value so the function can correctly set all four border edges even when the diff is partial.

One notable property: noSelect. This controls whether a box's cells are excluded from text selection in fullscreen mode. The 'from-left-edge' variant extends the exclusion from column 0 to the box's right edge for every row — specifically designed so that clicking and dragging over a diff panel doesn't accidentally copy line-number prefixes and diff sigils into the clipboard.

§6 design-system/color.ts — Bridging Themes to Raw Colors

This file is tiny but it is the architectural glue. It is the only place where theme key strings are resolved to raw color values:

export function color(
  c: keyof Theme | Color | undefined,
  theme: ThemeName,
  type: ColorType = 'foreground',
): (text: string) => string {
  return text => {
    if (!c) return text

    // Raw color values bypass theme lookup entirely
    if (
      c.startsWith('rgb(') || c.startsWith('#') ||
      c.startsWith('ansi256(') || c.startsWith('ansi:')
    ) {
      return colorize(text, c, type)
    }

    // Theme key → raw color → chalk output
    return colorize(text, getTheme(theme)[c as keyof Theme], type)
  }
}

The return value is a curried function, not a string. This means components can create colorizers once (e.g., at the top of a render function) and reuse them across multiple text strings, avoiding repeated theme lookups. The check for raw color prefixes means you can pass either a theme key like "claude" or a raw color like "rgb(215,119,87)" — both work transparently.

§7 The /theme and /color Commands

The /theme Command

The /theme command renders a full interactive ThemePicker component inside a Pane (wrapped with color="permission" — which is the blue/purple permission-request color, making the picker visually distinct from normal output):

// commands/theme/theme.tsx
export const call: LocalJSXCommandCall = async (onDone, _context) => {
  return <ThemePickerCommand onDone={onDone} />
}

function ThemePickerCommand({ onDone }: Props) {
  const [, setTheme] = useTheme()

  return (
    <Pane color="permission">
      <ThemePicker
        onThemeSelect={setting => {
          setTheme(setting)
          onDone(`Theme set to ${setting}`)
        }}
        onCancel={() => onDone('Theme picker dismissed', { display: 'system' })}
        skipExitHandling={true}
      />
    </Pane>
  )
}

The ThemePicker component itself (in components/ThemePicker.tsx) provides live preview — you can arrow through themes and see the UI re-render before committing. This works via a usePreviewTheme() hook that sets a temporary theme state distinct from the saved setting. Pressing Escape cancels and restores the previous theme; pressing Enter saves it.

The /color Command

The /color command is for sub-agent sessions only — it sets the color of the prompt bar for the current session, creating visual differentiation when multiple Claude Code agents are running simultaneously:

// commands/color/color.ts
export async function call(onDone, context, args) {
  // Teammates cannot set their own color — only the team leader assigns them
  if (isTeammate()) {
    onDone('Cannot set color: This session is a swarm teammate...', { display: 'system' })
    return null
  }

  const colorArg = args.trim().toLowerCase()

  // 'default', 'reset', 'none', 'gray', 'grey' all reset to gray
  if (RESET_ALIASES.includes(colorArg)) {
    await saveAgentColor(sessionId, 'default', fullPath)
    // Updates AppState for immediate effect
    context.setAppState(prev => ({
      ...prev,
      standaloneAgentContext: { ...prev.standaloneAgentContext, color: undefined }
    }))
    return null
  }

  // Valid colors: AGENT_COLORS = ['red','blue','green','yellow','purple','orange','pink','cyan']
  await saveAgentColor(sessionId, colorArg, fullPath)
  context.setAppState(prev => ({
    ...prev,
    standaloneAgentContext: { ...prev.standaloneAgentContext, color: colorArg }
  }))
}
Key detail

The color is saved to the transcript file (saveAgentColor(sessionId, colorArg, fullPath)) for persistence across session restarts, and also applied immediately via setAppState. The "default" sentinel is not an empty string — it uses the literal string "default" so that the truthiness guard in sessionStorage.ts still persists the reset. An empty string would be falsy and might not be written.

§8 AgentColorManager — Sub-agent Visual Identity

In multi-agent (swarm) sessions, Claude Code needs to visually distinguish agents from each other. The agentColorManager.ts handles the mapping from agent type strings to theme color slots:

export const AGENT_COLORS: readonly AgentColorName[] = [
  'red', 'blue', 'green', 'yellow',
  'purple', 'orange', 'pink', 'cyan'
]

export const AGENT_COLOR_TO_THEME_COLOR = {
  red:    'red_FOR_SUBAGENTS_ONLY',
  blue:   'blue_FOR_SUBAGENTS_ONLY',
  // ... maps human-readable name → Theme key
} as const satisfies Record<AgentColorName, keyof Theme>

satisfies 제약이 핵심입니다 — 상수의 타입을 Record<AgentColorName, keyof Theme>으로 확장하지 않으면서, 맵의 모든 항목이 Theme 타입의 유효한 키를 가리키는지 컴파일 타임에 보장합니다. 누군가 새 에이전트 색상을 추가하면서 Theme 타입에 해당 _FOR_SUBAGENTS_ONLY 슬롯을 추가하는 것을 잊으면 빌드가 실패합니다.

general-purpose 에이전트 타입은 getAgentColor에서 undefined를 반환합니다 — 의도적으로 색상이 없는데, 범용 세션은 시각적으로 차별화되지 않기 때문입니다. 코드 리뷰, 테스팅 등 특화된 에이전트 타입만 풀에서 색상이 할당됩니다.

§9 전체 색상 해석 데이터 흐름

flowchart TD A[User selects theme via /theme] --> B[useTheme setTheme called] B --> C[ThemeSetting saved to settings.json] C --> D[ThemeName resolved at runtime
auto → dark or light] D --> E[getTheme ThemeName returns Theme object] E --> F[Component calls color fn
from design-system/color.ts] F --> G{Is c a raw color?
starts with rgb/hash/ansi} G -- Yes --> H[colorize directly] G -- No --> I[getTheme theme key
lookup raw value] I --> H[colorize raw color string, type] H --> J{chalk.level} J -- 3 truecolor --> K[chalk.rgb / chalk.hex] J -- 2 256-color --> L[chalk.ansi256] J -- 0/1 no color --> M[plain string] K --> N[ANSI escape sequence to terminal] L --> N M --> N subgraph Module Load Time O[boostChalkLevelForXtermJs
TERM_PROGRAM=vscode AND level=2 → level=3] P[clampChalkLevelForTmux
TMUX AND level gt 2 → level=2] O --> P end

§10 심층 분석

왜 TypeScript 색상 객체 대신 RGB 문자열인가?

색상 시스템은 모든 색상 값을 구조화된 객체가 아닌 문자열("rgb(215,119,87)", "#d77757", "ansi:red")로 저장합니다. 코드 스멜처럼 보일 수 있지만 실질적인 장점이 있습니다:

  • Theme 타입은 단지 { [key: string]: string }입니다 — 설정 저장을 위한 JSON 직렬화가 간단합니다.
  • Color TypeScript 타입은 템플릿 리터럴 타입(`rgb(${number},${number},${number})`)을 사용하여 런타임 파싱 없이 컴파일 타임 검증을 달성합니다.
  • 문자열 접두사(rgb(, #, ansi:)는 자기 설명적 판별자입니다 — 별도의 타입 태그 없이 분기할 수 있습니다.
  • Chalk는 이미 문자열(hex, rgb, ansi 색상 이름)을 기대합니다 — 객체로 래핑하면 이점 없이 언래핑 단계만 추가됩니다.

유일한 비용: 색상 적용마다 colorize()에서의 정규식 파싱. 하지만 색상 적용은 이미 터미널 I/O를 수행하는 렌더 루프에서 발생하므로, 정규식 비용은 I/O에 비해 무시할 수 있습니다.

shimmer 애니메이션 패턴

많은 테마 토큰이 쌍으로 존재합니다: claude / claudeShimmer, inactive / inactiveShimmer 등. shimmer 변형은 항상 약간 더 밝거나(다크 모드에서) 약간 더 채도가 높습니다 — 컴포넌트가 두 값 사이를 오갈 때 거친 깜빡임이 아닌 "호흡" 또는 "펄스" 애니메이션으로 읽히도록 조정되었습니다.

예를 들어: claude = rgb(215,119,87)(Claude 오렌지)와 claudeShimmer = rgb(235,159,127) — 각 채널에서 20단위 더 밝아, 시각적으로 구별되기에는 충분하지만 다른 색상처럼 보이지는 않습니다.

ultrathink 키워드의 레인보우 색상도 같은 패턴을 따릅니다: 7개 색조 × 2개 가중치(기본 + shimmer) = 14개 테마 슬롯. Claude Code가 메시지에서 "ultrathink"이라는 단어를 감지하면 레인보우 색상을 순환합니다. shimmer 변형은 순환에 교대 강도를 포함시켜 효과를 더 역동적으로 만듭니다.

Apple Terminal과 256색 폴백

colorize.ts가 VS Code 부스트와 tmux 클램핑을 처리하는 한편, utils/theme.ts 자체에 별도의 Apple Terminal 처리가 있습니다:

// Create a chalk instance with 256-color level for Apple Terminal
// Apple Terminal doesn't handle 24-bit color escape sequences well
const chalkForChart =
  env.terminal === 'Apple_Terminal'
    ? new Chalk({ level: 2 }) // 256 colors
    : chalk

export function themeColorToAnsi(themeColor: string): string {
  const rgbMatch = themeColor.match(/rgb\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)/)
  if (rgbMatch) {
    const colored = chalkForChart.rgb(r, g, b)('X')
    return colored.slice(0, colored.indexOf('X'))
  }
}

이 함수는 특별히 asciichart 렌더링(UI의 비용/토큰 사용량 그래프)에 사용됩니다. chalk의 전역 레벨 감지에 의존하는 대신, Apple Terminal을 위해 레벨 2로 고정된 별도의 Chalk 인스턴스를 생성합니다. "이스케이프 시퀀스 추출" 트릭 — 단일 문자 'X'를 렌더링하고 그 앞의 모든 것을 잘라내는 것 — 은 chalk가 공개 API로 노출하지 않는 오프닝 SGR 시퀀스를 얻는 영리한 방법입니다.

왜 /color 명령어가 swarm 팀원에게 금지되는가

swarm(멀티 에이전트) 세션에서는 "팀 리더" Claude Code 인스턴스와 하나 이상의 "팀원" 인스턴스가 있습니다. 팀 리더는 AgentColorManager를 통해 팀원에게 색상을 할당합니다 — 이렇게 UI가 멀티 에이전트 트랜스크립트에서 어떤 에이전트가 말하고 있는지 색상으로 구분할 수 있습니다.

팀원이 직접 /color를 호출할 수 있다면, 팀 리더의 색상 할당과 충돌하거나 덮어쓸 수 있어 swarm 디스플레이의 시각적 일관성이 깨질 수 있습니다. color.ts의 가드:

if (isTeammate()) {
  onDone('Cannot set color: This session is a swarm teammate.
Teammate colors are assigned by the team leader.', { display: 'system' })
  return null
}

isTeammate() 체크는 bootstrap/state.ts에서 읽습니다 — 세션은 시작 시 팀원으로 실행되었는지 알고 있습니다. 이는 어떤 사용자 상호작용 전에 설정되므로, 에이전트가 첫 번째 동작으로 /color를 시도하더라도 가드가 안정적입니다.

핵심 요점

  • 테마 시스템은 세 가지 뚜렷한 단계로 구성됩니다: 팔레트 정의(theme.ts의 6개 명명된 Theme 객체), 환경 정규화(모듈 로드 시 colorize.ts에서의 chalk 레벨 클램핑/부스팅), 그리고 렌더링(컴포넌트 렌더 시점의 테마 키 → 원시 색상 → chalk 출력).
  • colorize.ts의 두 가지 chalk 레벨 조정 — VS Code 부스트와 tmux 클램핑 — 은 import 시 한 번 실행되어 이후 모든 색상 출력에 영향을 미칩니다. 순서는 의도적입니다: 부스트가 먼저이므로 vscode 안의 tmux가 올바르게 재클램핑됩니다.
  • 테마 해석은 색상 렌더링과 분리됩니다: styles.tscolorize.ts는 테마를 전혀 인식하지 못합니다. 오직 design-system/color.ts만이 테마 키와 원시 값을 연결합니다.
  • daltonized 테마는 모든 시맨틱 성공/diff-추가 토큰에서 녹색을 파란색으로 대체합니다 — 개별적 색상 조정이 아닌 체계적인 접근성 결정입니다.
  • Theme 타입의 _FOR_SUBAGENTS_ONLYShimmer 네이밍 규칙은 런타임 체크가 아닌 네이밍 규율로 강제되는 의도적 가드레일입니다.
  • /color 명령어는 세션 재시작 시에도 리셋이 유지되도록 빈 문자열이 아닌 센티널 문자열 "default"를 사용하여 트랜스크립트 파일에 저장합니다.

이해도 확인

Q1. boostChalkLevelForXtermJs()가 해결하는 문제는 무엇인가요?
Q2. tmux 클램핑이 VS Code 부스트 이후에 실행되는 이유는 무엇인가요?
Q3. daltonized 테마에서 success 색상이 다른 녹색 색조가 아닌 파란색으로 변경된 이유는 무엇인가요?
Q4. design-system/color.ts가 색상이 적용된 문자열이 아닌 커링된 함수를 반환하는 이유는 무엇인가요?
Q5. /color reset이 호출될 때, 빈 문자열 대신 "default"를 트랜스크립트에 저장하는 이유는 무엇인가요?